Copilot commented on code in PR #14002:
URL: https://github.com/apache/apisix/pull/14002#discussion_r4165844804


##########
apisix/plugins/signing/xml.lua:
##########
@@ -0,0 +1,358 @@
+--
+-- 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")
+
+
+ffi.cdef[[
+typedef unsigned char xmlChar;
+typedef int xmlElementType;
+typedef struct _xmlDoc xmlDoc;
+typedef xmlDoc *xmlDocPtr;
+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;
+
+struct _xmlNs {
+    xmlNsPtr next;
+    xmlElementType type;
+    const xmlChar *href;
+    const xmlChar *prefix;
+    void *_private;
+    xmlDocPtr context;
+};
+
+struct _xmlAttr {
+    void *_private;
+    xmlElementType type;
+    const xmlChar *name;
+    xmlNodePtr children;
+    xmlNodePtr last;
+    xmlNodePtr parent;
+    xmlAttrPtr next;
+    xmlAttrPtr prev;
+    xmlDocPtr doc;
+    xmlNsPtr ns;
+    int atype;
+    void *psvi;
+};
+
+struct _xmlNode {
+    void *_private;
+    xmlElementType 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;
+};
+
+void xmlInitParser(void);
+xmlDocPtr xmlReadMemory(const char *buffer, int size, const char *URL,
+                        const char *encoding, int options);
+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);
+int xmlReconciliateNs(xmlDocPtr doc, xmlNodePtr tree);
+
+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 xmlNewNode(xmlNsPtr ns, const xmlChar *name);
+xmlNodePtr xmlNewChild(xmlNodePtr parent, xmlNsPtr ns,
+                       const xmlChar *name, const xmlChar *content);
+xmlNodePtr xmlAddPrevSibling(xmlNodePtr cur, xmlNodePtr elem);
+void xmlUnlinkNode(xmlNodePtr cur);
+void xmlFreeNode(xmlNodePtr cur);
+
+xmlAttrPtr xmlSetNsProp(xmlNodePtr node, xmlNsPtr ns,
+                        const xmlChar *name, const xmlChar *value);
+xmlAttrPtr xmlSetProp(xmlNodePtr node, const xmlChar *name,
+                      const xmlChar *value);
+xmlChar *xmlGetNsProp(const xmlNodePtr node, const xmlChar *name,
+                      const xmlChar *name_space);
+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
+    C = ffi.load("libxml2.so.2")
+end
+C.xmlInitParser()
+
+local XML_ELEMENT_NODE = 1
+local XML_PARSE_NONET = 2048
+local EXCLUSIVE_C14N = 1
+
+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
+
+
+function _M.parse(value)
+    if type(value) ~= "string" or value == "" then
+        return nil, "XML body is empty"
+    end
+
+    local upper = value:upper()
+    if upper:find("<!DOCTYPE", 1, true) or upper:find("<!ENTITY", 1, true) then
+        return nil, "DTD and entity declarations are not allowed"

Review Comment:
   This raw byte scan can be bypassed by valid non-ASCII XML encodings. For 
example, a UTF-16 document contains NUL bytes between the characters of 
`<!DOCTYPE`, so this check misses it and libxml2 proceeds to parse and sign a 
DTD-bearing document, contrary to the documented rejection policy. Inspect the 
parsed document for an internal/external subset (or explicitly reject 
unsupported encodings) rather than relying on an ASCII substring search.



##########
apisix/plugins/signing/xml.lua:
##########
@@ -0,0 +1,358 @@
+--
+-- 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")
+
+
+ffi.cdef[[
+typedef unsigned char xmlChar;
+typedef int xmlElementType;
+typedef struct _xmlDoc xmlDoc;
+typedef xmlDoc *xmlDocPtr;
+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;
+
+struct _xmlNs {
+    xmlNsPtr next;
+    xmlElementType type;
+    const xmlChar *href;
+    const xmlChar *prefix;
+    void *_private;
+    xmlDocPtr context;
+};
+
+struct _xmlAttr {
+    void *_private;
+    xmlElementType type;
+    const xmlChar *name;
+    xmlNodePtr children;
+    xmlNodePtr last;
+    xmlNodePtr parent;
+    xmlAttrPtr next;
+    xmlAttrPtr prev;
+    xmlDocPtr doc;
+    xmlNsPtr ns;
+    int atype;
+    void *psvi;
+};
+
+struct _xmlNode {
+    void *_private;
+    xmlElementType 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;
+};
+
+void xmlInitParser(void);
+xmlDocPtr xmlReadMemory(const char *buffer, int size, const char *URL,
+                        const char *encoding, int options);
+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);
+int xmlReconciliateNs(xmlDocPtr doc, xmlNodePtr tree);
+
+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 xmlNewNode(xmlNsPtr ns, const xmlChar *name);
+xmlNodePtr xmlNewChild(xmlNodePtr parent, xmlNsPtr ns,
+                       const xmlChar *name, const xmlChar *content);
+xmlNodePtr xmlAddPrevSibling(xmlNodePtr cur, xmlNodePtr elem);
+void xmlUnlinkNode(xmlNodePtr cur);
+void xmlFreeNode(xmlNodePtr cur);
+
+xmlAttrPtr xmlSetNsProp(xmlNodePtr node, xmlNsPtr ns,
+                        const xmlChar *name, const xmlChar *value);
+xmlAttrPtr xmlSetProp(xmlNodePtr node, const xmlChar *name,
+                      const xmlChar *value);
+xmlChar *xmlGetNsProp(const xmlNodePtr node, const xmlChar *name,
+                      const xmlChar *name_space);
+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
+    C = ffi.load("libxml2.so.2")
+end
+C.xmlInitParser()
+
+local XML_ELEMENT_NODE = 1
+local XML_PARSE_NONET = 2048
+local EXCLUSIVE_C14N = 1
+
+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
+
+
+function _M.parse(value)
+    if type(value) ~= "string" or value == "" then
+        return nil, "XML body is empty"
+    end
+
+    local upper = value:upper()
+    if upper:find("<!DOCTYPE", 1, true) or upper:find("<!ENTITY", 1, true) then
+        return nil, "DTD and entity declarations are not allowed"
+    end
+
+    local doc = C.xmlReadMemory(value, #value, "document.xml", nil, 
XML_PARSE_NONET)

Review Comment:
   `xmlReadMemory` uses libxml2's default error handlers here. On malformed 
input, libxml2 writes the offending XML line to stderr (for example, a 
mismatched tag error includes the complete line), and OpenResty redirects 
worker stderr into the error log. SOAP/XML bodies can contain secrets, so add 
`XML_PARSE_NOERROR | XML_PARSE_NOWARNING` and keep logging only the sanitized 
error code already emitted by the request pipeline.



##########
apisix/plugins/signing/request.lua:
##########
@@ -0,0 +1,88 @@
+--
+-- 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 core = require("apisix.core")
+
+local ngx = ngx
+
+local _M = {}
+
+
+local function contains(values, expected)
+    for _, value in ipairs(values) do
+        if value == expected then
+            return true
+        end
+    end
+    return false
+end
+
+
+local function normalized_content_type(value)
+    if not value then
+        return ""
+    end
+    local normalized = value:match("^%s*([^;]+)")
+    return normalized and normalized:lower() or ""
+end
+
+
+function _M.rewrite(plugin_name, conf, ctx, sign)
+    if type(conf) ~= "table" then
+        return 500, {message = "internal_error"}
+    end
+
+    conf.request = conf.request or {}
+    conf.request.methods = conf.request.methods or {"POST"}
+    conf.request.content_types = conf.request.content_types or 
{"application/xml"}
+    conf.request.max_body_bytes = conf.request.max_body_bytes or 5242880
+
+    if not contains(conf.request.methods, ngx.req.get_method()) then
+        return
+    end
+
+    local content_type = normalized_content_type(
+        core.request.header(ctx, "Content-Type"))
+    if not contains(conf.request.content_types, content_type) then
+        return 415, {message = "unsupported_media_type"}
+    end
+
+    local body, body_err = core.request.get_body(conf.request.max_body_bytes, 
ctx)
+    if not body then
+        core.log.error(plugin_name, " failed to read request body")
+        return body_err and 413 or 400,
+               {message = body_err and "body_too_large" or "empty_body"}
+    end
+
+    if conf.signature.algorithm == "rsa-sha1" then
+        core.log.warn(plugin_name, " is using legacy SHA-1 cryptography")
+    end

Review Comment:
   This warning executes for every signed request. On a route that 
intentionally enables legacy SHA-1, normal traffic therefore produces one 
warning-level log entry per request, which can saturate error-log I/O and 
storage. Emit this once per worker/configuration (or rate-limit it) rather than 
placing an unconditional warning on the request hot path.



##########
apisix/plugins/soap-signer/signer.lua:
##########
@@ -0,0 +1,337 @@
+--
+-- 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 random = require("resty.random")
+local resty_string = require("resty.string")
+
+local algorithms = require("apisix.plugins.signing.algorithms")
+local credentials = require("apisix.plugins.signing.credentials")
+local xml = require("apisix.plugins.signing.xml")
+
+local encode_base64 = ngx.encode_base64
+
+local SOAP11 = "http://schemas.xmlsoap.org/soap/envelope/";
+local SOAP12 = "http://www.w3.org/2003/05/soap-envelope";
+local WSSE = "http://docs.oasis-open.org/wss/2004/01/";
+             .. "oasis-200401-wss-wssecurity-secext-1.0.xsd"
+local WSU = "http://docs.oasis-open.org/wss/2004/01/";
+            .. "oasis-200401-wss-wssecurity-utility-1.0.xsd"
+local DS = "http://www.w3.org/2000/09/xmldsig#";
+local EXCLUSIVE_C14N = "http://www.w3.org/2001/10/xml-exc-c14n#";
+local BASE64_ENCODING = "http://docs.oasis-open.org/wss/2004/01/";
+                        .. 
"oasis-200401-wss-soap-message-security-1.0#Base64Binary"
+local X509_V3 = "http://docs.oasis-open.org/wss/2004/01/";
+                .. "oasis-200401-wss-x509-token-profile-1.0#X509v3"
+
+local _M = {
+    namespaces = {
+        soap11 = SOAP11,
+        soap12 = SOAP12,
+        wsse = WSSE,
+        wsu = WSU,
+        ds = DS,
+    },
+}
+
+
+local function new_id(prefix)
+    local value, err = random.bytes(16, true)
+    if not value then
+        return nil, err
+    end
+    return prefix .. "-" .. resty_string.to_hex(value)
+end
+
+
+local function find_envelope_parts(doc)
+    local root = xml.root(doc)
+    if not root or xml.name(root) ~= "Envelope" then
+        return nil, "not_soap", "root element is not SOAP Envelope"
+    end
+
+    local soap_namespace = xml.namespace_uri(root)
+    if soap_namespace ~= SOAP11 and soap_namespace ~= SOAP12 then
+        return nil, "not_soap", "unsupported SOAP namespace"
+    end
+
+    local body = xml.find_child(root, soap_namespace, "Body")
+    if not body then
+        return nil, "invalid_soap", "SOAP Body is required"
+    end
+
+    local header = xml.find_child(root, soap_namespace, "Header")
+    if not header then
+        header = xml.new_node(body.ns, "Header")
+        xml.add_before(body, header)
+    end
+
+    return {
+        doc = doc,
+        root = root,
+        header = header,
+        body = body,
+        soap_namespace = soap_namespace,
+    }
+end
+
+
+local function ensure_namespaces(envelope)
+    envelope.soap_ns = xml.ensure_namespace(
+        envelope.doc, envelope.root, "soapenv", envelope.soap_namespace)
+    envelope.wsse_ns = xml.ensure_namespace(envelope.doc, envelope.root, 
"wsse", WSSE)
+    envelope.wsu_ns = xml.ensure_namespace(envelope.doc, envelope.root, "wsu", 
WSU)
+    envelope.ds_ns = xml.ensure_namespace(envelope.doc, envelope.root, "ds", 
DS)
+end
+
+
+local function get_id(node)
+    return xml.get_namespaced_property(node, WSU, "Id")
+end
+
+
+local function ensure_id(node, namespace, prefix)
+    local existing = get_id(node)
+    if existing and existing ~= "" then
+        return existing
+    end
+
+    local id, err = new_id(prefix)
+    if not id then
+        return nil, err
+    end
+    xml.set_namespaced_property(node, namespace, "Id", id)
+    return id
+end
+
+
+local function reject_duplicate_ids(root)
+    local seen = {}
+    local duplicate
+    xml.walk(root, function(node)
+        local id = get_id(node)
+        if id then
+            if seen[id] then
+                duplicate = id
+            end
+            seen[id] = true
+        end
+    end)
+    if duplicate then
+        return nil, "duplicate XML ID"
+    end
+    return true
+end
+
+
+local function timestamp_value(timestamp)
+    return os.date("!%Y-%m-%dT%H:%M:%SZ", timestamp)
+end
+
+
+local function build_timestamp(envelope, ttl_seconds, now)
+    local timestamp = xml.new_child(envelope.security, envelope.wsu_ns, 
"Timestamp")
+    local id, err = ensure_id(timestamp, envelope.wsu_ns, "TS")
+    if not id then
+        return nil, err
+    end
+    xml.new_child(timestamp, envelope.wsu_ns, "Created", timestamp_value(now))
+    xml.new_child(timestamp, envelope.wsu_ns, "Expires",
+                  timestamp_value(now + ttl_seconds))
+    return timestamp
+end
+
+
+local function build_token(envelope, runtime, key_info_mode)
+    if key_info_mode == "none" then
+        return
+    end
+
+    local token_id, err = new_id("X509")
+    if not token_id then
+        return nil, err
+    end
+
+    local token = xml.new_child(envelope.security, envelope.wsse_ns,
+                                "BinarySecurityToken",
+                                encode_base64(runtime.certificate_der))
+    xml.set_namespaced_property(token, envelope.wsu_ns, "Id", token_id)
+    xml.set_property(token, "EncodingType", BASE64_ENCODING)
+    xml.set_property(token, "ValueType", X509_V3)
+    return token_id
+end
+
+
+local function add_reference(envelope, signed_info, target, algorithm)
+    local canonical, canonical_err = xml.canonicalize(target)
+    if not canonical then
+        return nil, canonical_err
+    end
+    local digest_value, digest_err = algorithms.hash(canonical, algorithm)
+    if not digest_value then
+        return nil, digest_err
+    end
+
+    local reference = xml.new_child(signed_info, envelope.ds_ns, "Reference")
+    xml.set_property(reference, "URI", "#" .. get_id(target))
+    local transforms = xml.new_child(reference, envelope.ds_ns, "Transforms")
+    local transform = xml.new_child(transforms, envelope.ds_ns, "Transform")
+    xml.set_property(transform, "Algorithm", EXCLUSIVE_C14N)
+    local digest_method = xml.new_child(reference, envelope.ds_ns, 
"DigestMethod")
+    xml.set_property(digest_method, "Algorithm", algorithm.digest_uri)
+    xml.new_child(reference, envelope.ds_ns, "DigestValue", 
encode_base64(digest_value))
+    return true
+end
+
+
+local function build_signature(envelope, runtime, token_id, targets, algorithm,
+                               key_info_mode)
+    local signature = xml.new_child(envelope.security, envelope.ds_ns, 
"Signature")
+    local signed_info = xml.new_child(signature, envelope.ds_ns, "SignedInfo")
+
+    local canonicalization_method = xml.new_child(
+        signed_info, envelope.ds_ns, "CanonicalizationMethod")
+    xml.set_property(canonicalization_method, "Algorithm", EXCLUSIVE_C14N)
+    local signature_method = xml.new_child(signed_info, envelope.ds_ns, 
"SignatureMethod")
+    xml.set_property(signature_method, "Algorithm", algorithm.signature_uri)
+
+    for _, target in ipairs(targets) do
+        local ok, err = add_reference(envelope, signed_info, target, algorithm)
+        if not ok then
+            return nil, err
+        end
+    end
+
+    local canonical, canonical_err = xml.canonicalize(signed_info)
+    if not canonical then
+        return nil, canonical_err
+    end
+    local signature_value, signature_err = runtime.private_key:sign(
+        canonical, algorithm.digest)
+    if not signature_value then
+        return nil, signature_err
+    end
+    xml.new_child(signature, envelope.ds_ns, "SignatureValue",
+                  encode_base64(signature_value))
+
+    if key_info_mode == "binary_security_token" then
+        local key_info = xml.new_child(signature, envelope.ds_ns, "KeyInfo")
+        local token_reference = xml.new_child(key_info, envelope.wsse_ns,
+                                              "SecurityTokenReference")
+        local reference = xml.new_child(token_reference, envelope.wsse_ns, 
"Reference")
+        xml.set_property(reference, "URI", "#" .. token_id)
+        xml.set_property(reference, "ValueType", X509_V3)
+    end
+    return signature
+end
+
+
+local function set_must_understand(envelope, enabled)
+    local value = enabled and "1" or "0"
+    if envelope.soap_namespace == SOAP12 then
+        value = enabled and "true" or "false"
+    end
+    xml.set_namespaced_property(envelope.security, envelope.soap_ns,
+                                "mustUnderstand", value)
+end
+
+
+function _M.sign(body, conf, now)
+    local algorithm_name = conf.signature and conf.signature.algorithm or 
"rsa-sha256"
+    local algorithm = algorithms.get(algorithm_name)
+    if not algorithm then
+        return nil, "signing_error", "unsupported signature algorithm"
+    end
+    local key_info_mode = conf.signature
+                          and conf.signature.key_info or 
"binary_security_token"
+    if key_info_mode ~= "binary_security_token" and key_info_mode ~= "none" 
then
+        return nil, "signing_error", "unsupported KeyInfo mode"
+    end
+
+    local doc, parse_err = xml.parse(body)
+    if not doc then
+        return nil, "invalid_soap", parse_err
+    end
+
+    local envelope, code, envelope_err = find_envelope_parts(doc)
+    if not envelope then
+        xml.free_document(doc)
+        return nil, code, envelope_err
+    end
+
+    local actual_version = envelope.soap_namespace == SOAP12 and "1.2" or "1.1"
+    if conf.soap.version ~= "auto" and conf.soap.version ~= actual_version then
+        xml.free_document(doc)
+        return nil, "not_soap", "SOAP version does not match configuration"
+    end
+    if #xml.find_all(envelope.root, DS, "Signature") > 0 then
+        xml.free_document(doc)
+        return nil, "already_signed", "SOAP message already contains a 
signature"
+    end
+
+    local unique, unique_err = reject_duplicate_ids(envelope.root)
+    if not unique then
+        xml.free_document(doc)
+        return nil, "invalid_soap", unique_err
+    end
+
+    local runtime, credential_err = credentials.load(conf.credentials)
+    if not runtime then
+        xml.free_document(doc)
+        return nil, "credential_error", credential_err
+    end
+
+    ensure_namespaces(envelope)
+    envelope.security = xml.find_child(envelope.header, WSSE, "Security")
+    if not envelope.security then
+        envelope.security = xml.new_child(envelope.header, envelope.wsse_ns, 
"Security")
+    end

Review Comment:
   Reusing an arbitrary client-supplied `wsse:Security` block preserves its 
SOAP 1.1 `actor` or SOAP 1.2 `role`. If that block targets another role 
(including SOAP 1.2 `role/none`), the generated signature is placed in a header 
that the intended upstream receiver must ignore. Select only an 
untargeted/ultimate-receiver block after validating ambiguity, or create/reject 
the block according to an explicit role policy.



##########
apisix/plugins/soap-signer/signer.lua:
##########
@@ -0,0 +1,337 @@
+--
+-- 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 random = require("resty.random")
+local resty_string = require("resty.string")
+
+local algorithms = require("apisix.plugins.signing.algorithms")
+local credentials = require("apisix.plugins.signing.credentials")
+local xml = require("apisix.plugins.signing.xml")
+
+local encode_base64 = ngx.encode_base64
+
+local SOAP11 = "http://schemas.xmlsoap.org/soap/envelope/";
+local SOAP12 = "http://www.w3.org/2003/05/soap-envelope";
+local WSSE = "http://docs.oasis-open.org/wss/2004/01/";
+             .. "oasis-200401-wss-wssecurity-secext-1.0.xsd"
+local WSU = "http://docs.oasis-open.org/wss/2004/01/";
+            .. "oasis-200401-wss-wssecurity-utility-1.0.xsd"
+local DS = "http://www.w3.org/2000/09/xmldsig#";
+local EXCLUSIVE_C14N = "http://www.w3.org/2001/10/xml-exc-c14n#";
+local BASE64_ENCODING = "http://docs.oasis-open.org/wss/2004/01/";
+                        .. 
"oasis-200401-wss-soap-message-security-1.0#Base64Binary"
+local X509_V3 = "http://docs.oasis-open.org/wss/2004/01/";
+                .. "oasis-200401-wss-x509-token-profile-1.0#X509v3"
+
+local _M = {
+    namespaces = {
+        soap11 = SOAP11,
+        soap12 = SOAP12,
+        wsse = WSSE,
+        wsu = WSU,
+        ds = DS,
+    },
+}
+
+
+local function new_id(prefix)
+    local value, err = random.bytes(16, true)
+    if not value then
+        return nil, err
+    end
+    return prefix .. "-" .. resty_string.to_hex(value)
+end
+
+
+local function find_envelope_parts(doc)
+    local root = xml.root(doc)
+    if not root or xml.name(root) ~= "Envelope" then
+        return nil, "not_soap", "root element is not SOAP Envelope"
+    end
+
+    local soap_namespace = xml.namespace_uri(root)
+    if soap_namespace ~= SOAP11 and soap_namespace ~= SOAP12 then
+        return nil, "not_soap", "unsupported SOAP namespace"
+    end
+
+    local body = xml.find_child(root, soap_namespace, "Body")
+    if not body then
+        return nil, "invalid_soap", "SOAP Body is required"
+    end

Review Comment:
   This selects the first SOAP `Body` without rejecting additional sibling 
`Body` elements. A request can therefore be forwarded with one body covered by 
the signature and another unsigned body; SOAP stacks that select a different 
occurrence become vulnerable to a signature-wrapping ambiguity. SOAP permits 
exactly one `Body`, so count direct SOAP-namespace children and return 
`invalid_soap` on duplicates before signing.



-- 
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