VYPR
Medium severityGHSA Advisory· Published May 17, 2024

onelogin/php-saml signature wrapping attacks

CVE-2016-1000253

Description

Vulnerability in onelogin/php-saml versions prior to 2.10.0 allows signature Wrapping attacks which may result in a malicious user gaining unauthorized access to a system.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

The one-login/php-saml library prior to v2.10.0 is vulnerable to signature wrapping attacks that can allow unauthenticated access.

Vulnerability

Overview

The vulnerability in onelogin/php-saml versions prior to 2.10.0 allows a signature wrapping attack on SAML assertions [1]. The root cause is insufficient validation of signed elements within the SAML response; the library's isValid() method originally checked for signed Assertions by a direct string comparison (in_array('Assertion', $signedElements)) without accounting for the possibility of duplicate or nested signed elements [2]. An attacker could craft a SAML response containing a valid signature on an outer element (e.g., the Response) while embedding a malicious, unsigned Assertion that would be processed by the service provider, thereby bypassing signature verification [1][2].

Exploitation

Exploitation requires the ability to send a crafted SAML response to a service provider using a vulnerable version of the library (prior to 2.10.0) [1]. No prior authentication is needed; the attack is performed remotely by an adversary who intercepts or forges SAML messages. The fix introduced in commit 9d31baa checks for both signed Response and signed Assertion tags separately and throws an exception if neither is present, preventing the acceptance of unsigned assertions in signed messages [2].

Impact

A successful signature wrapping attack allows a malicious user to gain unauthorized access to systems relying on the vulnerable php-saml library for authentication [1]. The attacker can impersonate any user by crafting a fake SAML assertion that appears to be validated by the trusted IdP, effectively bypassing single sign-on security.

Mitigation

Users must upgrade to php-saml version 2.10.0 or later to protect against signature wrapping [1][3]. The patch enforces strict validation of signed elements and rejects responses that lack a legitimate signature on either the Response or the Assertion [2]. No workaround is provided for earlier versions.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
onelogin/php-samlPackagist
< 2.10.02.10.0

Affected products

1

Patches

1
9d31baa97a57

Improve Signature validation process. Validates NameID only if strict is enabled

https://github.com/onelogin/php-samlSixto MartinOct 14, 2016via ghsa
4 files changed · +79 38
  • lib/Saml2/Response.php+64 33 modified
    @@ -109,6 +109,12 @@ public function isValid($requestId = null)
     
                 $signedElements = $this->processSignedElements();
     
    +            $responseTag = '{'.OneLogin_Saml2_Constants::NS_SAMLP.'}Response';
    +            $assertionTag = '{'.OneLogin_Saml2_Constants::NS_SAML.'}Assertion';
    +
    +            $hasSignedResponse = in_array($responseTag, $signedElements);
    +            $hasSignedAssertion = in_array($assertionTag, $signedElements);
    +
                 if ($this->_settings->isStrict()) {
                     $security = $this->_settings->getSecurityData();
     
    @@ -259,41 +265,40 @@ public function isValid($requestId = null)
                         throw new Exception("A valid SubjectConfirmation was not found on this Response");
                     }
     
    -                if ($security['wantAssertionsSigned'] && !in_array('Assertion', $signedElements)) {
    +                if ($security['wantAssertionsSigned'] && !$hasSignedAssertion) {
                         throw new Exception("The Assertion of the Response is not signed and the SP requires it");
                     }
                     
    -                if ($security['wantMessagesSigned'] && !in_array('Response', $signedElements)) {
    +                if ($security['wantMessagesSigned'] && !$hasSignedResponse) {
                         throw new Exception("The Message of the Response is not signed and the SP requires it");
                     }
                 }
     
    -            if (!empty($signedElements)) {
    +            // Detect case not supported
    +            if ($this->encrypted) {
    +                $encryptedIDNodes = OneLogin_Saml2_Utils::query($this->decryptedDocument, '/samlp:Response/saml:Assertion/saml:Subject/saml:EncryptedID');
    +                if ($encryptedIDNodes->length > 0) {
    +                    throw new Exception('Unsigned SAML Response that contains a signed and encrypted Assertion with encrypted nameId is not supported.');
    +                }
    +            }
    +
    +            if (empty($signedElements) || (!$hasSignedResponse && !$hasSignedAssertion)) {
    +                throw new Exception('No Signature found. SAML Response rejected');
    +            } else {
                     $cert = $idpData['x509cert'];
                     $fingerprint = $idpData['certFingerprint'];
                     $fingerprintalg = $idpData['certFingerprintAlgorithm'];
     
                     # If find a Signature on the Response, validates it checking the original response
    -                if (in_array('Response', $signedElements)) {
    -                    $documentToValidate = $this->document;
    -                } else {
    -                    # Otherwise validates the assertion (decrypted assertion if was encrypted)
    -                    if ($this->encrypted) {
    -                        $documentToValidate = $this->decryptedDocument;
    -                        $encryptedIDNodes = OneLogin_Saml2_Utils::query($this->decryptedDocument, '/samlp:Response/saml:Assertion/saml:Subject/saml:EncryptedID');
    -                        if ($encryptedIDNodes->length > 0) {
    -                            throw new Exception('Unsigned SAML Response that contains a signed and encrypted Assertion with encrypted nameId is not supported.');
    -                        }
    -                    } else {
    -                        $documentToValidate = $this->document;
    -                    }
    +                if ($hasSignedResponse && !OneLogin_Saml2_Utils::validateSign($this->document, $cert, $fingerprint, $fingerprintalg, OneLogin_Saml2_Utils::RESPONSE_SIGNATURE_XPATH)) {
    +                    throw new Exception("Signature validation failed. SAML Response rejected");
                     }
     
    -                if (!OneLogin_Saml2_Utils::validateSign($documentToValidate, $cert, $fingerprint, $fingerprintalg)) {
    -                    throw new Exception('Signature validation failed. SAML Response rejected');
    +                # If find a Signature on the Assertion (decrypted assertion if was encrypted)
    +                $documentToCheckAssertion = $this->encrypted ? $this->decryptedDocument : $this->document;
    +                if ($hasSignedAssertion && !OneLogin_Saml2_Utils::validateSign($documentToCheckAssertion, $cert, $fingerprint, $fingerprintalg, OneLogin_Saml2_Utils::ASSERTION_SIGNATURE_XPATH)) {
    +                    throw new Exception("Signature validation failed. SAML Response rejected");
                     }
    -            } else {
    -                throw new Exception('No Signature found. SAML Response rejected');
                 }
                 return true;
             } catch (Exception $e) {
    @@ -385,7 +390,7 @@ public function getIssuers()
         {
             $issuers = array();
     
    -        $responseIssuer = $this->_query('/samlp:Response/saml:Issuer');
    +        $responseIssuer = OneLogin_Saml2_Utils::query($this->document, '/samlp:Response/saml:Issuer');
             if ($responseIssuer->length == 1) {
                 $issuers[] = $responseIssuer->item(0)->textContent;
             } else {
    @@ -435,14 +440,14 @@ public function getNameIdData()
                     throw new Exception("Not NameID found in the assertion of the Response");
                 }
             } else {
    -            if (empty($nameId->nodeValue)) {
    +            if ($this->_settings->isStrict() && empty($nameId->nodeValue)) {
                     throw new Exception("An empty NameID value found");
                 }
                 $nameIdData['Value'] = $nameId->nodeValue;
     
                 foreach (array('Format', 'SPNameQualifier', 'NameQualifier') as $attr) {
                     if ($nameId->hasAttribute($attr)) {
    -                    if ($attr == 'SPNameQualifier') {
    +                    if ($this->_settings->isStrict() && $attr == 'SPNameQualifier') {
                             $spData = $this->_settings->getSPData();
                             $spEntityId = $spData['entityId'];
                             if ($spEntityId != $nameId->getAttribute($attr)) {
    @@ -531,16 +536,13 @@ public function getAttributes()
             */
     
             $entries = $this->_queryAssertion('/saml:AttributeStatement/saml:Attribute');
    -        $processedAttrNames = array();
     
             /** @var $entry DOMNode */
             foreach ($entries as $entry) {
                 $attributeName = $entry->attributes->getNamedItem('Name')->nodeValue;
     
    -            if (in_array($attributeName, $processedAttrNames)) {
    +            if (in_array($attributeName, array_keys($attributes))) {
                     throw new Exception("Found an Attribute element with duplicated Name");
    -            } else {
    -                $processedAttrNames[] = $attributeName;
                 }
     
                 $attributeValues = array();
    @@ -595,9 +597,13 @@ public function processSignedElements()
                 $signNodes = $this->document->getElementsByTagName('Signature');
             }
             foreach ($signNodes as $signNode) {
    -            $signedElement = $signNode->parentNode->localName;
    +            
    +            $responseTag = '{'.OneLogin_Saml2_Constants::NS_SAMLP.'}Response';
    +            $assertionTag = '{'.OneLogin_Saml2_Constants::NS_SAML.'}Assertion';
    +
    +            $signedElement = '{'.$signNode->parentNode->namespaceURI.'}'.$signNode->parentNode->localName;
     
    -            if ($signedElement != 'Response' && $signedElement != 'Assertion') {
    +            if ($signedElement != $responseTag && $signedElement != $assertionTag) {
                     throw new Exception('Invalid Signature Element '.$signedElement.' SAML Response rejected');
                 }
     
    @@ -680,13 +686,34 @@ public function validateSignedElements($signedElements)
             if (count($signedElements) > 2) {
                 return false;
             }
    +
    +        $responseTag = '{'.OneLogin_Saml2_Constants::NS_SAMLP.'}Response';
    +        $assertionTag = '{'.OneLogin_Saml2_Constants::NS_SAML.'}Assertion';
    +
             $ocurrence = array_count_values($signedElements);
    -        if ((in_array('Response', $signedElements) && $ocurrence['Response'] > 1) ||
    -            (in_array('Assertion', $signedElements) && $ocurrence['Assertion'] > 1) ||
    -            !in_array('Response', $signedElements) && !in_array('Assertion', $signedElements)
    +        if ((in_array($responseTag, $signedElements) && $ocurrence[$responseTag] > 1) ||
    +            (in_array($assertionTag, $signedElements) && $ocurrence[$assertionTag] > 1) ||
    +            !in_array($responseTag, $signedElements) && !in_array($assertionTag, $signedElements)
             ) {
                 return false;
             }
    +
    +        // Check that the signed elements found here, are the ones that will be verified
    +        // by OneLogin_Saml2_Utils->validateSign()
    +        if (in_array($responseTag, $signedElements)) {
    +            $expectedSignatureNodes = OneLogin_Saml2_Utils::query($this->document, OneLogin_Saml2_Utils::RESPONSE_SIGNATURE_XPATH);
    +            if ($expectedSignatureNodes->length != 1) {
    +                throw new Exception("Unexpected number of Response signatures found. SAML Response rejected.");
    +            }
    +        }
    +
    +        if (in_array($assertionTag, $signedElements)) {
    +            $expectedSignatureNodes = $this->_query(OneLogin_Saml2_Utils::ASSERTION_SIGNATURE_XPATH);
    +            if ($expectedSignatureNodes->length != 1) {
    +                throw new Exception("Unexpected number of Assertion signatures found. SAML Response rejected.");
    +            }
    +        }
    +
             return true;
         }
     
    @@ -752,7 +779,11 @@ protected function _queryAssertion($assertionXpath)
          */
         private function _query($query)
         {
    -        return OneLogin_Saml2_Utils::query($this->document, $query);
    +        if ($this->encrypted) {
    +            return OneLogin_Saml2_Utils::query($this->decryptedDocument, $query);
    +        } else {
    +            return OneLogin_Saml2_Utils::query($this->document, $query);
    +        }
         }
     
         /**
    
  • lib/Saml2/Utils.php+12 2 modified
    @@ -8,6 +8,8 @@
     
     class OneLogin_Saml2_Utils
     {
    +    const RESPONSE_SIGNATURE_XPATH = "/samlp:Response/ds:Signature";
    +    const ASSERTION_SIGNATURE_XPATH = "/samlp:Response/saml:Assertion/ds:Signature";
     
         /**
          * @var bool Control if the `Forwarded-For-*` headers are used
    @@ -1068,12 +1070,13 @@ public static function addSign($xml, $key, $cert, $signAlgorithm = XMLSecurityKe
          * @param string|null    $cert           The pubic cert
          * @param string|null    $fingerprint    The fingerprint of the public cert
          * @param string|null    $fingerprintalg The algorithm used to get the fingerprint
    +     * @param string|null    $xpath          The xpath of the signed element
          *
          * @return bool
          *
          * @throws Exception
          */
    -    public static function validateSign($xml, $cert = null, $fingerprint = null, $fingerprintalg = 'sha1')
    +    public static function validateSign($xml, $cert = null, $fingerprint = null, $fingerprintalg = 'sha1', $xpath=null)
         {
             if ($xml instanceof DOMDocument) {
                 $dom = clone $xml;
    @@ -1087,7 +1090,14 @@ public static function validateSign($xml, $cert = null, $fingerprint = null, $fi
             $objXMLSecDSig = new XMLSecurityDSig();
             $objXMLSecDSig->idKeys = array('ID');
     
    -        $objDSig = $objXMLSecDSig->locateSignature($dom);
    +        if ($xpath) {
    +            $nodeset = OneLogin_Saml2_Utils::query($dom, $xpath);
    +            $objDSig = $nodeset->item(0);
    +            $objXMLSecDSig->sigNode = $objDSig;
    +        } else {
    +            $objDSig = $objXMLSecDSig->locateSignature($dom);
    +        }
    +
             if (!$objDSig) {
                 throw new Exception('Cannot locate Signature Node');
             }
    
  • tests/src/OneLogin/Saml2/ResponseTest.php+2 2 modified
    @@ -236,7 +236,7 @@ public function testGetNameIdData()
          */
         public function testCheckOneCondition() {
             $xml = file_get_contents(TEST_ROOT . '/data/responses/invalids/no_conditions.xml.base64');
    -        $response = new OneLogin_Saml2_Response($this->_settings, $xml);    
    +        $response = new OneLogin_Saml2_Response($this->_settings, $xml);
             $this->assertFalse($response->checkOneCondition());
     
             $xml2 = file_get_contents(TEST_ROOT . '/data/responses/valid_response.xml.base64');
    @@ -251,7 +251,7 @@ public function testCheckOneCondition() {
          */
         public function testCheckOneAuthNStatement() {
             $xml = file_get_contents(TEST_ROOT . '/data/responses/invalids/no_authnstatement.xml.base64');
    -        $response = new OneLogin_Saml2_Response($this->_settings, $xml);    
    +        $response = new OneLogin_Saml2_Response($this->_settings, $xml);
             $this->assertFalse($response->checkOneAuthnStatement());
     
             $xml2 = file_get_contents(TEST_ROOT . '/data/responses/valid_response.xml.base64');
    
  • tests/src/OneLogin/Saml2/SignedResponseTest.php+1 1 modified
    @@ -46,7 +46,7 @@ public function testResponseAndAssertionSigned()
     
             $settingsInfo['idp']['entityId'] = "https://federate.example.net/saml/saml2/idp/metadata.php";
             $settingsInfo['sp']['entityId'] = "hello.com";
    -        $settings = new OneLogin_Saml2_Settings($settingsInfo);   
    +        $settings = new OneLogin_Saml2_Settings($settingsInfo);
     
             // Both the Response and the Asseretion are signed
             $message = file_get_contents(TEST_ROOT . '/data/responses/simple_saml_php.xml');
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.