Skip to content

Commit 39878dd

Browse files
committed
On this commit we changed the way we decrypt an Assertion and added 2 new
validations in order to avoid Signature wrapping attacks: - Extra validations at the validateSignedElements method to check that the ref URIs and IDs are unique and consistent. - Validate the document (encrypted and decrypted version) against the scheme.
1 parent 43845f1 commit 39878dd

File tree

1 file changed

+93
-26
lines changed

1 file changed

+93
-26
lines changed

lib/Saml2/Response.php

Lines changed: 93 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -107,31 +107,27 @@ public function isValid($requestId = null)
107107
$spData = $this->_settings->getSPData();
108108
$spEntityId = $spData['entityId'];
109109

110-
$signedElements = array();
111-
if ($this->encrypted) {
112-
$signNodes = $this->decryptedDocument->getElementsByTagName('Signature');
113-
} else {
114-
$signNodes = $this->document->getElementsByTagName('Signature');
115-
}
116-
foreach ($signNodes as $signNode) {
117-
$signedElements[] = $signNode->parentNode->localName;
118-
}
119-
120-
if (!empty($signedElements)) {
121-
// Check SignedElements
122-
if (!$this->validateSignedElements($signedElements)) {
123-
throw new Exception('Found an unexpected Signature Element. SAML Response rejected');
124-
}
125-
}
110+
$signedElements = $this->processSignedElements();
126111

127112
if ($this->_settings->isStrict()) {
128113
$security = $this->_settings->getSecurityData();
129114

130115
if ($security['wantXMLValidation']) {
116+
$errorXmlMsg = "Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd";
131117
$res = OneLogin_Saml2_Utils::validateXML($this->document, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive());
132118
if (!$res instanceof DOMDocument) {
133-
throw new Exception("Invalid SAML Response. Not match the saml-schema-protocol-2.0.xsd");
119+
throw new Exception($errorXmlMsg);
120+
}
121+
122+
# If encrypted, check also the decrypted document
123+
124+
if ($this->encrypted) {
125+
$res = OneLogin_Saml2_Utils::validateXML($this->decryptedDocument, 'saml-schema-protocol-2.0.xsd', $this->_settings->isDebugActive());
126+
if (!$res instanceof DOMDocument) {
127+
throw new Exception($errorXmlMsg);
128+
}
134129
}
130+
135131
}
136132

137133
$currentURL = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery();
@@ -273,7 +269,7 @@ public function isValid($requestId = null)
273269
$documentToValidate = $this->decryptedDocument;
274270
if ($this->encrypted) {
275271
$documentToValidate = $this->decryptedDocument;
276-
$encryptedIDNodes = OneLogin_Saml2_Utils::query($this->decryptedDocument, '/samlp:Response/saml:EncryptedAssertion/saml:Assertion/saml:Subject/saml:EncryptedID');
272+
$encryptedIDNodes = OneLogin_Saml2_Utils::query($this->decryptedDocument, '/samlp:Response/saml:Assertion/saml:Subject/saml:EncryptedID');
277273
if ($encryptedIDNodes->length > 0) {
278274
throw new Exception('Unsigned SAML Response that contains a signed and encrypted Assertion with encrypted nameId is not supported.');
279275
}
@@ -511,6 +507,71 @@ public function validateNumAssertions()
511507
return ($assertionNodes->length + $encryptedAssertionNodes->length == 1);
512508
}
513509

510+
/**
511+
* Verifies the signature nodes:
512+
* - Checks that are Response or Assertion
513+
* - Check that IDs and reference URI are unique and consistent.
514+
*
515+
* @return array Signed element tags
516+
*/
517+
public function processSignedElements()
518+
{
519+
$signedElements = array();
520+
$verifiedSeis = array();
521+
$verifiedIds = array();
522+
523+
if ($this->encrypted) {
524+
$signNodes = $this->decryptedDocument->getElementsByTagName('Signature');
525+
} else {
526+
$signNodes = $this->document->getElementsByTagName('Signature');
527+
}
528+
foreach ($signNodes as $signNode) {
529+
$signedElement = $signNode->parentNode->localName;
530+
531+
if ($signedElement != 'Response' && $signedElement != 'Assertion') {
532+
throw new Exception('Invalid Signature Element '.$signedElement.' SAML Response rejected');
533+
}
534+
535+
# Check that reference URI matches the parent ID and no duplicate References or IDs
536+
$idValue = $signNode->parentNode->getAttribute('ID');
537+
if (empty($idValue)) {
538+
throw new Exception('Signed Element must contain an ID. SAML Response rejected');
539+
}
540+
541+
if (in_array($idValue, $verifiedIds)) {
542+
throw new Exception('Duplicated ID. SAML Response rejected');
543+
}
544+
$verifiedIds[] = $idValue;
545+
546+
$ref = $signNode->getElementsByTagName('Reference');
547+
if (!empty($ref)) {
548+
$ref = $ref->item(0);
549+
$sei = $ref->getAttribute('URI');
550+
if (!empty($sei)) {
551+
$sei = substr($sei, 1);
552+
553+
if ($sei != $idValue) {
554+
throw new Exception('Found an invalid Signed Element. SAML Response rejected');
555+
}
556+
557+
if (in_array($sei, $verifiedSeis)) {
558+
throw new Exception('Duplicated Reference URI. SAML Response rejected');
559+
}
560+
$verifiedSeis[] = $sei;
561+
}
562+
}
563+
$signedElements[] = $signedElement;
564+
}
565+
566+
if (!empty($signedElements)) {
567+
// Check SignedElements
568+
if (!$this->validateSignedElements($signedElements)) {
569+
throw new Exception('Found an unexpected Signature Element. SAML Response rejected');
570+
}
571+
}
572+
return $signedElements;
573+
}
574+
514575
/**
515576
* Verifies that the document is still valid according Conditions Element.
516577
*
@@ -580,8 +641,7 @@ protected function _queryAssertion($assertionXpath)
580641
$xpath->registerNamespace('ds', OneLogin_Saml2_Constants::NS_DS);
581642
$xpath->registerNamespace('xenc', OneLogin_Saml2_Constants::NS_XENC);
582643

583-
$assertionNode = $this->encrypted ? '/samlp:Response/saml:EncryptedAssertion/saml:Assertion'
584-
: '/samlp:Response/saml:Assertion';
644+
$assertionNode = '/samlp:Response/saml:Assertion';
585645
$signatureQuery = $assertionNode . '/ds:Signature/ds:SignedInfo/ds:Reference';
586646
$assertionReferenceNode = $xpath->query($signatureQuery)->item(0);
587647
if (!$assertionReferenceNode) {
@@ -595,9 +655,9 @@ protected function _queryAssertion($assertionXpath)
595655
} else {
596656
$id = substr($assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue, 1);
597657
}
598-
$nameQuery = "/samlp:Response[@ID='$id']/".($this->encrypted? "saml:EncryptedAssertion/" : "")."saml:Assertion" . $assertionXpath;
658+
$nameQuery = "/samlp:Response[@ID='$id']/saml:Assertion" . $assertionXpath;
599659
} else {
600-
$nameQuery = "/samlp:Response/".($this->encrypted? "saml:EncryptedAssertion/" : "")."saml:Assertion" . $assertionXpath;
660+
$nameQuery = "/samlp:Response/saml:Assertion" . $assertionXpath;
601661
}
602662
} else {
603663
$uri = $assertionReferenceNode->attributes->getNamedItem('URI')->nodeValue;
@@ -668,11 +728,18 @@ protected function _decryptAssertion($dom)
668728
if (empty($objKey->key)) {
669729
$objKey->loadKey($key);
670730
}
671-
$decrypt = $objenc->decryptNode($objKey, true);
672-
if ($decrypt instanceof DOMDocument) {
673-
return $decrypt;
731+
732+
$decrypted = $objenc->decryptNode($objKey, true);
733+
734+
if ($decrypted instanceof DOMDocument) {
735+
return $decrypted;
674736
} else {
675-
return $decrypt->ownerDocument;
737+
$encryptedAssertion = $decrypted->parentNode;
738+
$container = $encryptedAssertion->parentNode;
739+
740+
$container->replaceChild($decrypted, $encryptedAssertion);
741+
742+
return $decrypted->ownerDocument;
676743
}
677744
}
678745

0 commit comments

Comments
 (0)