Skip to content

Commit 1c98e9e

Browse files
committed
Harden XMLSecurityDSig after static analysis
Close SSRF via IPv4-mapped IPv6, fail closed on unknown transforms, tighten same-document Reference URIs, and fix enveloped-signature / Object C14N handling without always detaching Signature from the caller DOM.
1 parent 3715eea commit 1c98e9e

10 files changed

Lines changed: 606 additions & 118 deletions

CHANGELOG.txt

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,38 @@ Features:
77
support the URI fragment)
88
- Change selected XMLSecurityDSig private properties and methods to protected
99
so the class can be extended (gdespirito). refs #152
10+
- Add XMLSecurityDSig::ENVELOPED constant for the enveloped-signature Transform
11+
URI; deprecate empty appendCert() stub (use add509Cert())
1012

1113
Security Improvements:
1214
- Harden add509Cert() URL fetching against SSRF: only http/https by default
1315
(file:// requires options['allow_file_scheme']); resolve the host and reject
1416
loopback/private/link-local/reserved/CGNAT addresses; disable HTTP redirects
17+
- Reject IPv4-mapped / IPv4-compatible IPv6 certificate URL hosts (e.g.
18+
::ffff:127.0.0.1, ::ffff:169.254.169.254): unwrap the embedded IPv4 and apply
19+
the same private/reserved/CGNAT checks (PHP's FILTER_FLAG_NO_* treats mapped
20+
addresses as public)
21+
- Fail closed on unknown Reference Transform algorithms; recognize
22+
enveloped-signature explicitly. validateReference() no longer always detaches
23+
the Signature from the caller DOM (that broke C14N of ds:Object targets and
24+
left half-mutated trees on failure); enveloped-signature removes Signature
25+
only when it is a proper descendant of the node being digested
26+
- Tighten same-document Reference URIs to empty URI and "#id" only (reject
27+
"?query", bare "#", and external URIs); share parsing between processRefNode()
28+
and getRefNodeID(). Same-document refs always omit comments
29+
- Reject hostile idKeys attribute names (no whitespace / XPath operators) via
30+
Utils\XPath::filterAttrName; invalid names throw
31+
- Reject duplicate CanonicalizationMethod / SignatureMethod under SignedInfo and
32+
duplicate DigestMethod / DigestValue / SignatureValue during verify
33+
- Reset the cached DOMXPath in locateSignature() so instance reuse across
34+
documents cannot fatal with "Node from wrong document"
35+
- verifyDocument() no longer permanently overwrites instance algorithm
36+
allowlists (restored in finally)
37+
- addReference() / addReferenceList() / sign() / add509Cert() throw instead of
38+
silently no-oping when SignedInfo / signature context is missing; sign()
39+
requires setCanonicalMethod() and a SignatureMethod element
40+
- Require correct namespace URIs for InclusiveNamespaces (exc-c14n) and XPath
41+
transform children (not localName alone)
1542
- Reject a DOCTYPE in decrypted XML (defense against entity-expansion / XXE in
1643
attacker-crafted encrypted content)
1744
- Reject a DOCTYPE in documents being signature-verified (locateSignature). Closes

src/Utils/XPath.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ class XPath
99
const LETTERS = '\w';
1010
const EXTENDED_ALPHANUMERIC = '-\w\d\s_:\.';
1111

12+
/* Attribute names only — no whitespace or XPath operators. */
13+
const EXTENDED_ALPHANUMERIC_STRICT = '-\w\d_:\.';
14+
1215
const SINGLE_QUOTE = '\'';
1316
const DOUBLE_QUOTE = '"';
1417
const ALL_QUOTES = '[\'"]';
@@ -37,7 +40,7 @@ public static function filterAttrValue($value, $quotes = self::ALL_QUOTES)
3740
*
3841
* @return string The filtered attribute name.
3942
*/
40-
public static function filterAttrName($name, $allow = self::EXTENDED_ALPHANUMERIC)
43+
public static function filterAttrName($name, $allow = self::EXTENDED_ALPHANUMERIC_STRICT)
4144
{
4245
return preg_replace('#[^'.$allow.']#', '', $name);
4346
}

0 commit comments

Comments
 (0)