keremoge commented on code in PR #14175:
URL: https://github.com/apache/apisix/pull/14175#discussion_r4512770783


##########
apisix/plugins/xml-signer/xml.lua:
##########
@@ -0,0 +1,793 @@
+--
+-- Licensed to the Apache Software Foundation (ASF) under one or more
+-- contributor license agreements.  See the NOTICE file distributed with
+-- this work for additional information regarding copyright ownership.
+-- The ASF licenses this file to You under the Apache License, Version 2.0
+-- (the "License"); you may not use this file except in compliance with
+-- the License.  You may obtain a copy of the License at
+--
+--     http://www.apache.org/licenses/LICENSE-2.0
+--
+-- Unless required by applicable law or agreed to in writing, software
+-- distributed under the License is distributed on an "AS IS" BASIS,
+-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+-- See the License for the specific language governing permissions and
+-- limitations under the License.
+--
+local ffi = require("ffi")
+local jit = require("jit")
+
+local byte = string.byte
+local find = string.find
+local gmatch = string.gmatch
+local sub = string.sub
+local ipairs = ipairs
+local table_concat = table.concat
+local table_sort = table.sort
+local tonumber = tonumber
+local type = type
+
+
+ffi.cdef[[
+typedef unsigned char xmlChar;
+typedef struct _xmlDoc xmlDoc;
+typedef xmlDoc *xmlDocPtr;
+typedef struct _xmlParserCtxt xmlParserCtxt;
+typedef xmlParserCtxt *xmlParserCtxtPtr;
+typedef struct _xmlError xmlError;
+typedef xmlError *xmlErrorPtr;
+typedef void (*xmlStructuredErrorFunc)(void *userData, xmlErrorPtr error);
+typedef struct _xmlSAXHandler xmlSAXHandler;
+typedef struct _xmlNode xmlNode;
+typedef xmlNode *xmlNodePtr;
+typedef struct _xmlNs xmlNs;
+typedef xmlNs *xmlNsPtr;
+typedef struct _xmlAttr xmlAttr;
+typedef xmlAttr *xmlAttrPtr;
+typedef struct _xmlNodeSet xmlNodeSet;
+typedef xmlNodeSet *xmlNodeSetPtr;
+typedef void (*xmlFreeFunc)(void *mem);
+extern xmlFreeFunc xmlFree;
+]]
+
+-- FFI types are process-wide, including types loaded by other XML plugins:
+-- struct bodies must only be declared once, or reloading this module fails
+-- with "attempt to redefine".
+if not ffi.sizeof("struct _xmlSAXHandler") then
+    ffi.cdef[[
+struct _xmlSAXHandler {
+    void *internalSubset;
+    void *isStandalone;
+    void *hasInternalSubset;
+    void *hasExternalSubset;
+    void *resolveEntity;
+    void *getEntity;
+    void *entityDecl;
+    void *notationDecl;
+    void *attributeDecl;
+    void *elementDecl;
+    void *unparsedEntityDecl;
+    void *setDocumentLocator;
+    void *startDocument;
+    void *endDocument;
+    void *startElement;
+    void *endElement;
+    void *reference;
+    void *characters;
+    void *ignorableWhitespace;
+    void *processingInstruction;
+    void *comment;
+    void *warning;
+    void *error;
+    void *fatalError;
+    void *getParameterEntity;
+    void *cdataBlock;
+    void *externalSubset;
+    unsigned int initialized;
+    void *_private;
+    void *startElementNs;
+    void *endElementNs;
+    xmlStructuredErrorFunc serror;
+};
+]]
+end
+
+if not ffi.sizeof("struct _xmlParserCtxt") then
+    ffi.cdef[[
+struct _xmlParserCtxt {
+    xmlSAXHandler *sax;
+};
+]]
+end
+
+if not ffi.sizeof("struct _xmlError") then
+    ffi.cdef[[
+struct _xmlError {
+    int domain;
+    int code;
+    char *message;
+    int level;
+};
+]]
+end
+
+if not ffi.sizeof("struct _xmlNs") then
+    ffi.cdef[[
+struct _xmlNs {
+    xmlNsPtr next;
+    int type;
+    const xmlChar *href;
+    const xmlChar *prefix;
+    void *_private;
+    xmlDocPtr context;
+};
+]]
+end
+
+if not ffi.sizeof("struct _xmlAttr") then
+    ffi.cdef[[
+struct _xmlAttr {
+    void *_private;
+    int type;
+    const xmlChar *name;
+    xmlNodePtr children;
+    xmlNodePtr last;
+    xmlNodePtr parent;
+    xmlAttrPtr next;
+    xmlAttrPtr prev;
+    xmlDocPtr doc;
+    xmlNsPtr ns;
+    int atype;
+    void *psvi;
+};
+]]
+end
+
+if not ffi.sizeof("struct _xmlNode") then
+    ffi.cdef[[
+struct _xmlNode {
+    void *_private;
+    int type;
+    const xmlChar *name;
+    xmlNodePtr children;
+    xmlNodePtr last;
+    xmlNodePtr parent;
+    xmlNodePtr next;
+    xmlNodePtr prev;
+    xmlDocPtr doc;
+    xmlNsPtr ns;
+    xmlChar *content;
+    xmlAttrPtr properties;
+    xmlNsPtr nsDef;
+    void *psvi;
+    unsigned short line;
+    unsigned short extra;
+};
+]]
+end
+
+ffi.cdef[[
+void xmlInitParser(void);
+xmlParserCtxtPtr xmlNewParserCtxt(void);
+xmlDocPtr xmlCtxtReadMemory(xmlParserCtxtPtr ctxt, const char *buffer, int 
size,
+                            const char *URL, const char *encoding, int 
options);
+void xmlFreeParserCtxt(xmlParserCtxtPtr ctxt);
+void xmlFreeDoc(xmlDocPtr cur);
+xmlNodePtr xmlDocGetRootElement(const xmlDoc *doc);
+xmlDocPtr xmlNewDoc(const xmlChar *version);
+xmlNodePtr xmlDocCopyNode(xmlNodePtr node, xmlDocPtr doc, int extended);
+xmlNodePtr xmlDocSetRootElement(xmlDocPtr doc, xmlNodePtr root);
+
+xmlNsPtr xmlNewNs(xmlNodePtr node, const xmlChar *href, const xmlChar *prefix);
+xmlNsPtr xmlSearchNs(xmlDocPtr doc, xmlNodePtr node, const xmlChar 
*name_space);
+xmlNsPtr xmlSearchNsByHref(xmlDocPtr doc, xmlNodePtr node, const xmlChar 
*href);
+xmlNodePtr xmlNewChild(xmlNodePtr parent, xmlNsPtr ns,
+                       const xmlChar *name, const xmlChar *content);
+void xmlUnlinkNode(xmlNodePtr cur);
+void xmlFreeNode(xmlNodePtr cur);
+
+xmlAttrPtr xmlSetProp(xmlNodePtr node, const xmlChar *name,
+                      const xmlChar *value);
+xmlChar *xmlGetProp(const xmlNodePtr node, const xmlChar *name);
+xmlChar *xmlNodeGetContent(const xmlNodePtr cur);
+
+int xmlC14NDocDumpMemory(xmlDocPtr doc, xmlNodeSetPtr nodes,
+                         int mode, xmlChar **inclusive_ns_prefixes,
+                         int with_comments, xmlChar **doc_txt_ptr);
+void xmlDocDumpMemoryEnc(xmlDocPtr out_doc, xmlChar **doc_txt_ptr,
+                         int *doc_txt_len, const char *txt_encoding);
+]]
+
+local loaded, C = pcall(ffi.load, "xml2")
+if not loaded then
+    local lib
+    loaded, lib = pcall(ffi.load, "libxml2.so.2")
+    if not loaded then
+        error("libxml2 is not available, install the libxml2 package "
+              .. "(for example `apt-get install -y libxml2`): " .. 
tostring(lib), 0)
+    end
+    C = lib
+end
+C.xmlInitParser()
+
+local XML_ELEMENT_NODE = 1
+local XML_DTD_NODE = 14
+local XML_ERR_ERROR = 2
+local XML_PARSE_NONET = 2048
+local XML_PARSE_NOERROR = 32
+local XML_PARSE_NOWARNING = 64
+local EXCLUSIVE_C14N = 1
+
+local DTD_ERROR = "DTD and entity declarations are not allowed"
+
+-- libxml2 2.9 (the version in Debian bookworm based APISIX images) compares
+-- every attribute of an element with the ones before it and resolves every
+-- element and attribute name against all namespace declarations in scope.
+-- Many attributes on one element, or many namespace declarations in scope,
+-- therefore cost super-linear CPU time while the NGINX worker is blocked.
+-- Reject such documents before they reach the parser; real documents stay
+-- far below these limits.
+local MAX_ATTRIBUTES_PER_ELEMENT = 256
+local MAX_NAMESPACES_IN_SCOPE = 128
+-- The shortest possible attribute is 5 bytes (` a=""`), so only a markup
+-- segment longer than this can hold too many attributes.
+local MIN_SUSPICIOUS_SEGMENT = MAX_ATTRIBUTES_PER_ELEMENT * 5
+local ATTRIBUTE_PATTERN = "%s[^%s=<>\"']+%s*=%s*[\"']"
+local NAMESPACE_DECLARATION_PATTERN = "%sxmlns[%s:=]"
+
+local _M = {}
+
+
+local function xml_char(value)
+    if value == nil then
+        return nil
+    end
+    return ffi.cast("const xmlChar *", value)
+end
+
+
+local function copy_and_free(value)
+    if value == nil then
+        return nil
+    end
+    local result = ffi.string(value)
+    C.xmlFree(value)
+    return result
+end
+
+
+local function count_plain(value, needle, first, last, limit)
+    local count = 0
+    local position = first
+    while count <= limit do
+        local found = find(value, needle, position, true)
+        if not found or found > last then
+            break
+        end
+        count = count + 1
+        position = found + 1
+    end
+    return count
+end
+
+
+-- Attribute values cannot contain "<", so every attribute of an element is
+-- inside the segment between its "<" and the next "<". Counting may include
+-- text that looks like attributes, which only makes the check stricter.
+local function has_too_many_attributes(value)
+    local start = find(value, "<", 1, true)
+    while start do
+        local next_start = find(value, "<", start + 1, true)
+        local stop = (next_start or #value + 1) - 1
+        if stop - start >= MIN_SUSPICIOUS_SEGMENT
+           and count_plain(value, "=", start, stop, MAX_ATTRIBUTES_PER_ELEMENT)
+               > MAX_ATTRIBUTES_PER_ELEMENT then
+            local count = 0
+            for _ in gmatch(sub(value, start, stop), ATTRIBUTE_PATTERN) do
+                count = count + 1
+                if count > MAX_ATTRIBUTES_PER_ELEMENT then
+                    return true
+                end
+            end
+        end
+        start = next_start
+    end
+    return false
+end
+
+
+local function count_namespace_declarations(chunk)
+    if not find(chunk, "xmlns", 1, true) then
+        return 0
+    end
+    local count = 0
+    for _ in gmatch(chunk, NAMESPACE_DECLARATION_PATTERN) do
+        count = count + 1
+    end
+    return count
+end
+
+
+-- Track the namespace declarations in scope while walking the markup.
+-- Quoted attribute values, comments, CDATA sections and processing
+-- instructions are skipped, so only real declarations are counted; markup
+-- that cannot be scanned is left to libxml2, which rejects it.
+local function has_too_many_namespaces_in_scope(value)
+    local declared = {}
+    local depth = 0
+    local in_scope = 0
+    local position = 1
+    while true do
+        local start = find(value, "<", position, true)
+        if not start then
+            return false
+        end
+        local next_char = byte(value, start + 1)
+        if next_char == 33 then                                    -- "<!"
+            if sub(value, start, start + 3) == "<!--" then
+                local close = find(value, "-->", start + 4, true)
+                if not close then
+                    return false
+                end
+                position = close + 3
+            elseif sub(value, start, start + 8) == "<![CDATA[" then
+                local close = find(value, "]]>", start + 9, true)
+                if not close then
+                    return false
+                end
+                position = close + 3
+            else
+                return false, DTD_ERROR
+            end
+        elseif next_char == 63 then                                -- "<?"
+            local close = find(value, "?>", start + 2, true)
+            if not close then
+                return false
+            end
+            position = close + 2
+        elseif next_char == 47 then                                -- "</"
+            local close = find(value, ">", start + 2, true)
+            if not close then
+                return false
+            end
+            if depth > 0 then
+                in_scope = in_scope - declared[depth]
+                declared[depth] = nil
+                depth = depth - 1
+            end
+            position = close + 1
+        else                                                       -- start tag
+            local declarations = 0
+            local cursor = start + 1
+            local tag_end
+            while true do
+                local stop = find(value, "[\"'>]", cursor)
+                if not stop then
+                    return false
+                end
+                declarations = declarations + count_namespace_declarations(
+                    sub(value, cursor - 1, stop - 1))
+                local stop_char = byte(value, stop)
+                if stop_char == 62 then                            -- ">"
+                    tag_end = stop
+                    break
+                end
+                local close_quote = find(value, stop_char == 34 and '"' or "'",
+                                         stop + 1, true)
+                if not close_quote then
+                    return false
+                end
+                cursor = close_quote + 1
+            end
+            if in_scope + declarations > MAX_NAMESPACES_IN_SCOPE then
+                return true
+            end
+            if byte(value, tag_end - 1) ~= 47 then                -- not "/>"
+                depth = depth + 1
+                declared[depth] = declarations
+                in_scope = in_scope + declarations
+            end
+            position = tag_end + 1
+        end
+    end
+end
+
+
+-- Returns an error message if the document exceeds the parser limits.
+local function check_parser_limits(value)
+    if has_too_many_attributes(value) then
+        return "an element has more than " .. MAX_ATTRIBUTES_PER_ELEMENT
+               .. " attributes"
+    end
+    -- Without enough declarations in the whole document, none can be
+    -- exceeded in scope; this keeps the common case cheap.
+    if count_plain(value, "xmlns", 1, #value, MAX_NAMESPACES_IN_SCOPE)
+       > MAX_NAMESPACES_IN_SCOPE then
+        local exceeded, err = has_too_many_namespaces_in_scope(value)
+        if err then
+            return err
+        end
+        if exceeded then
+            return "more than " .. MAX_NAMESPACES_IN_SCOPE
+                   .. " namespace declarations are in scope"
+        end
+    end
+end
+
+
+-- libxml2 reports recoverable errors, such as an undeclared namespace prefix,
+-- through the structured error handler while still returning a document, so
+-- a missing handler would let namespace-malformed XML be signed. LuaJIT has a
+-- fixed number of FFI callback slots, so the handler is created once per
+-- module load. Parsing is synchronous, so a module-level flag is safe.
+local parse_failed = false
+local structured_error = ffi.cast("xmlStructuredErrorFunc", function(_, error)
+    if error ~= nil and error.level >= XML_ERR_ERROR then
+        parse_failed = true
+    end
+end)
+
+
+local function read_memory(ctxt, value, encoding)
+    parse_failed = false
+    ctxt.sax.serror = structured_error
+    local doc = C.xmlCtxtReadMemory(
+        ctxt, value, #value, "document.xml", encoding,
+        XML_PARSE_NONET + XML_PARSE_NOERROR + XML_PARSE_NOWARNING)
+    return doc, parse_failed
+end
+-- A C function that calls back into Lua must not be called from a
+-- JIT-compiled trace, otherwise LuaJIT raises "bad callback".
+jit.off(read_memory)
+
+
+function _M.parse(value, encoding)
+    if type(value) ~= "string" or value == "" then
+        return nil, "XML body is empty"
+    end
+    local limit_err = check_parser_limits(value)
+    if limit_err then
+        return nil, limit_err
+    end
+
+    local ctxt = C.xmlNewParserCtxt()
+    if ctxt == nil then
+        return nil, "failed to create XML parser context"
+    end
+
+    local doc, has_error = read_memory(ctxt, value, encoding)
+    C.xmlFreeParserCtxt(ctxt)
+    if doc == nil or has_error then
+        if doc ~= nil then
+            C.xmlFreeDoc(doc)
+        end
+        return nil, "failed to parse XML"
+    end
+
+    local child = ffi.cast("xmlNodePtr", doc).children
+    while child ~= nil do
+        if child.type == XML_DTD_NODE then
+            C.xmlFreeDoc(doc)
+            return nil, DTD_ERROR
+        end
+        child = child.next
+    end
+
+    ffi.gc(doc, C.xmlFreeDoc)
+    return doc
+end
+
+
+function _M.free_document(doc)
+    if doc ~= nil then
+        ffi.gc(doc, nil)
+        C.xmlFreeDoc(doc)
+    end
+end
+
+
+function _M.root(doc)
+    return C.xmlDocGetRootElement(doc)
+end
+
+
+function _M.name(node)
+    if node == nil or node.name == nil then
+        return nil
+    end
+    return ffi.string(node.name)
+end
+
+
+function _M.namespace_uri(node)
+    if node == nil or node.ns == nil or node.ns.href == nil then
+        return ""
+    end
+    return ffi.string(node.ns.href)
+end
+
+
+function _M.each_child(parent)
+    local current = parent ~= nil and parent.children or nil
+    return function()
+        while current ~= nil do
+            local node = current
+            current = current.next
+            if node.type == XML_ELEMENT_NODE then
+                return node
+            end
+        end
+    end
+end
+
+
+function _M.walk(root, visit)
+    visit(root)
+    for child in _M.each_child(root) do
+        _M.walk(child, visit)
+    end
+end
+
+
+function _M.find_child(parent, namespace_uri, local_name)
+    for child in _M.each_child(parent) do
+        if _M.name(child) == local_name and _M.namespace_uri(child) == 
namespace_uri then
+            return child
+        end
+    end
+end
+
+
+function _M.find_all(root, namespace_uri, local_name)
+    local result = {}
+    _M.walk(root, function(node)
+        if _M.name(node) == local_name and _M.namespace_uri(node) == 
namespace_uri then
+            result[#result + 1] = node
+        end
+    end)
+    return result
+end
+
+
+function _M.ensure_namespace(doc, node, prefix, href)
+    local by_href = C.xmlSearchNsByHref(doc, node, xml_char(href))
+    if by_href ~= nil and by_href.prefix ~= nil then
+        return by_href
+    end
+
+    local namespaces = {}
+    local current = node
+    while current ~= nil and current.type == XML_ELEMENT_NODE do
+        local ns = current.nsDef
+        while ns ~= nil do
+            if ns.prefix ~= nil then
+                local ns_prefix = ffi.string(ns.prefix)
+                if namespaces[ns_prefix] == nil then
+                    namespaces[ns_prefix] = ns
+                end
+            end
+            ns = ns.next
+        end
+        current = current.parent
+    end
+
+    local suffix = 0
+    while true do
+        local candidate = suffix == 0 and prefix or prefix .. suffix
+        local existing = namespaces[candidate]
+        if existing == nil then
+            return C.xmlNewNs(node, xml_char(href), xml_char(candidate))
+        end
+        if existing.href ~= nil and ffi.string(existing.href) == href then
+            return existing
+        end
+        suffix = suffix + 1
+    end
+end
+
+
+function _M.new_namespace(node, href, prefix)
+    return C.xmlNewNs(node, xml_char(href), xml_char(prefix))
+end
+
+
+local function pointer_key(pointer)
+    return tonumber(ffi.cast("intptr_t", pointer))
+end
+
+
+local function prefix_token(ns)
+    if ns.prefix == nil then
+        return "#default"
+    end
+    return ffi.string(ns.prefix)
+end
+
+
+-- Exclusive canonicalization only renders namespace declarations that an
+-- element or attribute name uses. A prefix that appears only in content, such
+-- as `xsi:type="ns:Type"`, would otherwise not be covered by the signature.
+-- Returns, sorted, the prefixes ("#default" for the default namespace) of the
+-- declarations in scope at `node` or inside it that no element or attribute
+-- name inside `node` uses, for the InclusiveNamespaces PrefixList.
+function _M.unused_namespace_prefixes(node)
+    local declarations = {}
+    local in_scope = {}
+    local used = {}
+
+    local function add(ns)
+        -- an undeclaration (xmlns="") binds nothing that could be changed
+        if ns.href ~= nil and ns.href[0] ~= 0 then
+            declarations[#declarations + 1] = ns
+        end
+    end
+
+    local function visit(element)
+        local ns = element.nsDef
+        while ns ~= nil do
+            add(ns)
+            ns = ns.next
+        end
+        if element.ns ~= nil then
+            used[pointer_key(element.ns)] = true
+        end
+        local attribute = element.properties
+        while attribute ~= nil do
+            if attribute.ns ~= nil then
+                used[pointer_key(attribute.ns)] = true
+            end
+            attribute = attribute.next
+        end
+    end
+
+    local ns = node.nsDef
+    while ns ~= nil do
+        in_scope[prefix_token(ns)] = true
+        ns = ns.next
+    end
+    local ancestor = node.parent
+    while ancestor ~= nil and ancestor.type == XML_ELEMENT_NODE do
+        ns = ancestor.nsDef
+        while ns ~= nil do
+            local token = prefix_token(ns)
+            if not in_scope[token] then
+                in_scope[token] = true
+                add(ns)
+            end
+            ns = ns.next
+        end
+        ancestor = ancestor.parent
+    end
+    _M.walk(node, visit)
+
+    local seen = {}
+    local prefixes = {}
+    for _, declaration in ipairs(declarations) do
+        local token = prefix_token(declaration)
+        if not used[pointer_key(declaration)] and not seen[token] then

Review Comment:
   fixed on 
https://github.com/apache/apisix/pull/14175/changes/d851448fac8a21ad5150ec31d017f8ed9ee73959



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to