Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,38 @@ Features:
support the URI fragment)
- Change selected XMLSecurityDSig private properties and methods to protected
so the class can be extended (gdespirito). refs #152
- Add XMLSecurityDSig::ENVELOPED constant for the enveloped-signature Transform
URI; deprecate empty appendCert() stub (use add509Cert())

Security Improvements:
- Harden add509Cert() URL fetching against SSRF: only http/https by default
(file:// requires options['allow_file_scheme']); resolve the host and reject
loopback/private/link-local/reserved/CGNAT addresses; disable HTTP redirects
- Reject IPv4-mapped / IPv4-compatible IPv6 certificate URL hosts (e.g.
::ffff:127.0.0.1, ::ffff:169.254.169.254): unwrap the embedded IPv4 and apply
the same private/reserved/CGNAT checks (PHP's FILTER_FLAG_NO_* treats mapped
addresses as public)
- Fail closed on unknown Reference Transform algorithms; recognize
enveloped-signature explicitly. validateReference() no longer always detaches
the Signature from the caller DOM (that broke C14N of ds:Object targets and
left half-mutated trees on failure); enveloped-signature removes Signature
only when it is a proper descendant of the node being digested
- Tighten same-document Reference URIs to empty URI and "#id" only (reject
"?query", bare "#", and external URIs); share parsing between processRefNode()
and getRefNodeID(). Same-document refs always omit comments
- Reject hostile idKeys attribute names (no whitespace / XPath operators) via
Utils\XPath::filterAttrName; invalid names throw
- Reject duplicate CanonicalizationMethod / SignatureMethod under SignedInfo and
duplicate DigestMethod / DigestValue / SignatureValue during verify
- Reset the cached DOMXPath in locateSignature() so instance reuse across
documents cannot fatal with "Node from wrong document"
- verifyDocument() no longer permanently overwrites instance algorithm
allowlists (restored in finally)
- addReference() / addReferenceList() / sign() / add509Cert() throw instead of
silently no-oping when SignedInfo / signature context is missing; sign()
requires setCanonicalMethod() and a SignatureMethod element
- Require correct namespace URIs for InclusiveNamespaces (exc-c14n) and XPath
transform children (not localName alone)
- Reject a DOCTYPE in decrypted XML (defense against entity-expansion / XXE in
attacker-crafted encrypted content)
- Reject a DOCTYPE in documents being signature-verified (locateSignature). Closes
Expand Down Expand Up @@ -97,6 +124,10 @@ Improvements:
- Update parameter type in XMLSecurityDSig::addReference() and addRefInternal()
- Tighten XMLSecEnc PHPDoc types for algorithm allowlists, references, and
encrypt/decrypt return values
- Harden XMLSecurityDSig DOM handling for static analysis: require DOMElement
before attribute access on XPath results, correct locateSignature /
reference / staticAdd509Cert PHPDocs, guard failed XPath queries and missing
SignatureMethod nodes, and remove dead null comparisons

Bug Fixes:
- Compact signature template (XMLSecurityDSig 'stripWhitespace' option) removes
Expand Down
5 changes: 4 additions & 1 deletion src/Utils/XPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class XPath
const LETTERS = '\w';
const EXTENDED_ALPHANUMERIC = '-\w\d\s_:\.';

/* Attribute names only — no whitespace or XPath operators. */

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are you trying to filter here? XML-attributes? With optional prefix?

const EXTENDED_ALPHANUMERIC_STRICT = '-\w\d_:\.';

const SINGLE_QUOTE = '\'';
const DOUBLE_QUOTE = '"';
const ALL_QUOTES = '[\'"]';
Expand Down Expand Up @@ -37,7 +40,7 @@ public static function filterAttrValue($value, $quotes = self::ALL_QUOTES)
*
* @return string The filtered attribute name.
*/
public static function filterAttrName($name, $allow = self::EXTENDED_ALPHANUMERIC)
public static function filterAttrName($name, $allow = self::EXTENDED_ALPHANUMERIC_STRICT)
{
return preg_replace('#[^'.$allow.']#', '', $name);
}
Expand Down
663 changes: 446 additions & 217 deletions src/XMLSecurityDSig.php

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions tests/cert-url-ssrf.phpt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ $cases = array(
'PRIVATE' => 'http://10.0.0.5/cert.pem',
'IPV6LOOP' => 'http://[::1]/cert.pem',
'CGNAT' => 'http://100.64.1.1/cert.pem',
'MAPPEDLOOP' => 'http://[::ffff:127.0.0.1]/cert.pem',
'MAPPEDPRIV' => 'http://[::ffff:10.0.0.1]/cert.pem',
'MAPPEDMETA' => 'http://[::ffff:169.254.169.254]/cert.pem',
);

foreach ($cases as $label => $url) {
Expand Down Expand Up @@ -47,4 +50,7 @@ LINKLOCAL: Certificate URL host is not allowed
PRIVATE: Certificate URL host is not allowed
IPV6LOOP: Certificate URL host is not allowed
CGNAT: Certificate URL host is not allowed
MAPPEDLOOP: Certificate URL host is not allowed
MAPPEDPRIV: Certificate URL host is not allowed
MAPPEDMETA: Certificate URL host is not allowed
FILE_OPTIN: OK
53 changes: 53 additions & 0 deletions tests/enveloped-signature-restored.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
Enveloped transform restores Signature before validating later references
--FILE--
<?php
require(dirname(__FILE__) . '/../xmlseclibs.php');
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecurityKey;

$doc = new DOMDocument();
$doc->loadXML('<Envelope><Payload>data</Payload></Envelope>');

$signer = new XMLSecurityDSig();
$signer->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$signer->addReference(
$doc,
XMLSecurityDSig::SHA256,
array(XMLSecurityDSig::ENVELOPED, XMLSecurityDSig::EXC_C14N),
array('force_uri' => true)
);

$object = $signer->addObject('inside-object');
$object->setAttribute('Id', 'obj1');
$signer->addReference(
$object,
XMLSecurityDSig::SHA256,
array(XMLSecurityDSig::EXC_C14N),
array('overwrite' => false)
);

$privateKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'private'));
$privateKey->loadKey(dirname(__FILE__) . '/privkey.pem', true);
$signer->sign($privateKey);
$signer->appendSignature($doc->documentElement);

$verifier = new XMLSecurityDSig();
$publicKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'public'));
$publicKey->loadKey(dirname(__FILE__) . '/mycert.pem', true);
$verifier->allowedSignatureAlgorithms = array(XMLSecurityKey::RSA_SHA256);
$verifier->allowedDigestAlgorithms = array(XMLSecurityDSig::SHA256);

try {
$nodes = $verifier->verifyDocument($publicKey, $doc);
echo "VERIFY: OK\n";
echo "SIGNATURES: ".$doc->getElementsByTagNameNS(XMLSecurityDSig::XMLDSIGNS, 'Signature')->length."\n";
echo isset($nodes['obj1']) ? "OBJECT: OK\n" : "OBJECT: missing\n";
} catch (Exception $e) {
echo "VERIFY: ".$e->getMessage()."\n";
}
?>
--EXPECTF--
VERIFY: OK
SIGNATURES: 1
OBJECT: OK
31 changes: 31 additions & 0 deletions tests/get-ref-node-id-external.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
getRefNodeID rejects external URIs consistently with processRefNode
--FILE--
<?php
require(dirname(__FILE__) . '/../xmlseclibs.php');
use RobRichards\XMLSecLibs\XMLSecurityDSig;

error_reporting(E_ALL);
$objDSig = new XMLSecurityDSig();

$cases = array(
'EXTERNAL' => 'http://example.com/data.xml#abc',
'QUERY' => '?x=1',
'BAREHASH' => '#',
'FRAGMENT' => '#abc',
'EMPTY' => '',
);

foreach ($cases as $label => $uri) {
$doc = new DOMDocument();
$doc->loadXML('<ds:Reference xmlns:ds="http://www.w3.org/2000/09/xmldsig#" URI="'.htmlspecialchars($uri, ENT_QUOTES).'"/>');
$id = $objDSig->getRefNodeID($doc->documentElement);
echo "$label: ".var_export($id, true)."\n";
}
?>
--EXPECTF--
EXTERNAL: NULL
QUERY: NULL
BAREHASH: NULL
FRAGMENT: 'abc'
EMPTY: NULL
40 changes: 40 additions & 0 deletions tests/idkeys-attr-name.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
--TEST--
idKeys attribute names cannot inject XPath operators
--FILE--
<?php
require(dirname(__FILE__) . '/../xmlseclibs.php');
use RobRichards\XMLSecLibs\XMLSecurityDSig;

$xml = <<<XML
<?xml version="1.0"?>
<Root>
<A Id="target">one</A>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:Reference URI="#target">
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
<ds:DigestValue>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>AA==</ds:SignatureValue>
</ds:Signature>
</Root>
XML;

$doc = new DOMDocument();
$doc->loadXML($xml);
$objXMLSecDSig = new XMLSecurityDSig();
$objXMLSecDSig->idKeys = array('foo or bar');
$objXMLSecDSig->locateSignature($doc);
$objXMLSecDSig->canonicalizeSignedInfo();
try {
$objXMLSecDSig->validateReference();
print "INJECT: unexpected success\n";
} catch (Exception $e) {
print "INJECT: ".$e->getMessage()."\n";
}
?>
--EXPECTF--
INJECT: Invalid idKeys attribute name
31 changes: 31 additions & 0 deletions tests/locate-signature-reuse.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
--TEST--
locateSignature resets XPath context across documents
--FILE--
<?php
require(dirname(__FILE__) . '/../xmlseclibs.php');
use RobRichards\XMLSecLibs\XMLSecurityDSig;

$sigXml = '<Root><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod/></ds:SignedInfo></ds:Signature></Root>';

$doc1 = new DOMDocument();
$doc1->loadXML($sigXml);

$doc2 = new DOMDocument();
$doc2->loadXML(str_replace('<Root>', '<Other>', str_replace('</Root>', '</Other>', $sigXml)));

$objDSig = new XMLSecurityDSig();
/* Prime an XPath context against the constructor template document. */
$objDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$objDSig->locateSignature($doc1);
$objDSig->canonicalizeSignedInfo();

try {
$objDSig->locateSignature($doc2);
$objDSig->canonicalizeSignedInfo();
echo "REUSE: OK\n";
} catch (Throwable $e) {
echo "REUSE: ".$e->getMessage()."\n";
}
?>
--EXPECTF--
REUSE: OK
53 changes: 53 additions & 0 deletions tests/object-ref-nested-signature.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
Object Id under nested Signature resolves before enveloped strip
--FILE--
<?php
require(dirname(__FILE__) . '/../xmlseclibs.php');
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecurityKey;

$doc = new DOMDocument();
$doc->loadXML('<Envelope><Payload>data</Payload></Envelope>');

$objDSig = new XMLSecurityDSig();
$objDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$objDSig->idKeys = array('xml:id');

$wrapped = $objDSig->sigNode->ownerDocument->createElement('Wrapped');
$wrapped->setAttributeNS('http://www.w3.org/XML/1998/namespace', 'xml:id', 'obj1');
$wrapped->appendChild($objDSig->sigNode->ownerDocument->createTextNode('inside-object'));
$objDSig->addObject($wrapped);

$objDSig->addReference(
$wrapped,
XMLSecurityDSig::SHA256,
array(XMLSecurityDSig::ENVELOPED, XMLSecurityDSig::EXC_C14N),
array(
'id_name' => 'id',
'overwrite' => false,
'prefix' => 'xml',
'prefix_ns' => 'http://www.w3.org/XML/1998/namespace',
)
);

$key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'private'));
$key->loadKey(dirname(__FILE__) . '/privkey.pem', true);
$objDSig->sign($key);
$objDSig->appendSignature($doc->documentElement);

$verify = new XMLSecurityDSig();
$verify->idKeys = array('xml:id');
$pub = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'public'));
$pub->loadKey(dirname(__FILE__) . '/mycert.pem', true);
$verify->allowedSignatureAlgorithms = array(XMLSecurityKey::RSA_SHA256);
$verify->allowedDigestAlgorithms = array(XMLSecurityDSig::SHA256);

try {
$nodes = $verify->verifyDocument($pub, $doc);
echo isset($nodes['obj1']) ? "OBJECT_REF: OK\n" : "OBJECT_REF: missing node\n";
} catch (Exception $e) {
echo "OBJECT_REF: ".$e->getMessage()."\n";
}
?>
--EXPECTF--
OBJECT_REF: OK
53 changes: 53 additions & 0 deletions tests/signedinfo-cardinality.phpt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
--TEST--
Duplicate SignedInfo children are rejected
--FILE--
<?php
require(dirname(__FILE__) . '/../xmlseclibs.php');
use RobRichards\XMLSecLibs\XMLSecurityDSig;

function checkStruct($xml, $label) {
$doc = new DOMDocument();
$doc->loadXML($xml);
$objXMLSecDSig = new XMLSecurityDSig();
$objXMLSecDSig->locateSignature($doc);
try {
$objXMLSecDSig->canonicalizeSignedInfo();
print "$label: unexpected success\n";
} catch (Exception $e) {
print "$label: ".$e->getMessage()."\n";
}
}

$dupCanon = <<<XML
<?xml version="1.0"?>
<Root>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
</ds:SignedInfo>
<ds:SignatureValue>AA==</ds:SignatureValue>
</ds:Signature>
</Root>
XML;
checkStruct($dupCanon, 'DUP_CANON');

$dupSigMethod = <<<XML
<?xml version="1.0"?>
<Root>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
</ds:SignedInfo>
<ds:SignatureValue>AA==</ds:SignatureValue>
</ds:Signature>
</Root>
XML;
checkStruct($dupSigMethod, 'DUP_SIGMETHOD');
?>
--EXPECTF--
DUP_CANON: Invalid structure - Too many CanonicalizationMethod elements found
DUP_SIGMETHOD: Invalid structure - Too many SignatureMethod elements found
Loading
Loading