GitHub Security Advisory: GHSA-r353-4845-pr5p
Package: simplesamlphp/xml-security (COMPOSER)
Severity: HIGH (CVSS 8.2)
CWE: CWE-354: Improper Validation of Integrity Check Value
Vulnerable versions: >= 2.0.0, < 2.3.1
Patched in: 2.3.1

Summary: simplesamlphp/xml-security: Missing AES-GCM Authentication Tag Validation on Encrypted Nodes Allows for Unauthorized Decryption

Details:
### Summary

XML nodes encrypted with either aes-128-gcm, aes-192-gcm, or aes-256-gcm lack validation of the authentication tag length.
An attacker can use this to brute-force an authentication tag, recover the [GHASH key](https://en.wikipedia.org/wiki/Galois/Counter_Mode#:~:text=%29%20is%20the-,hash%20key,-%2C%20a%20string%20of), and decrypt the encrypted nodes.
It also allows to forge arbitrary ciphertexts without knowing the encryption key.

### Details

When decrypting with either aes-128-gcm, aes-192-gcm, or aes-256-gcm [here](https://github.com/robrichards/xmlseclibs/blob/2bdfd742624d739dfadbd415f00181b4a77aaf07/src/XMLSecurityKey.php#L467-L479), the $authTag is set from a substr(), but never has its length validated (it should be validated with something like strlen($authTag) == self::AUTHTAG_LENGTH).
For that reason, a shorter than expected data blob will allow for the $authTag to have as short a tag as only one byte (see [PHP's documentation](https://www.php.net/manual/en/function.openssl-decrypt.php#:~:text=The%20length%20of%20the%20tag%20is%20not%20checked%20by%20the%20function.%20It%20is%20the%20caller%27s%20responsibility%20to%20ensure%20that%20the%20length%20of%20the%20tag%20matches%20the%20length%20of%20the%20tag%20retrieved%20when%20openssl_encrypt()%20has%20been%20called.%20Otherwise%20the%20decryption%20may%20succeed%20if%20the%20given%20tag%20only%20matches%20the%20start%20of%20the%20proper%20tag.)).

See this example:

```php
function test($data) {
    $ivSize = 12;
    $tagSize = 16;

    $iv = substr($data, 0, $ivSize);
    $data = substr($data, $ivSize);
    $offset = 0 - $tagSize;
    $tag = substr($data, $offset);
    $ct = substr($data, 0, $offset);

    echo 'IV: "' . $iv . '"' . PHP_EOL;
    echo 'Tag: "' . $tag . '"' . PHP_EOL;
    echo 'CT: "' . $ct . '"' . PHP_EOL;
}

/* Outputs:
php > test('myNonceNoncet');
IV: "myNonceNonce"
Tag: "t"
CT: ""
php > test('myNonceNonceta');
IV: "myNonceNonce"
Tag: "ta"
CT: ""
php > test('myNonceNoncetag');
IV: "myNonceNonce"
Tag: "tag"
CT: ""
*/
```

With a legit ciphertext in hand, this is enough to recover the [GHASH key](https://en.wikipedia.org/wiki/Galois/Counter_Mode#:~:text=%29%20is%20the-,hash%20key,-%2C%20a%20string%20of).
With that key, any authenticated tags can be computed offline which allows for decryption of the ciphertext and forgery of arbitrary ciphertexts.
PoC

1. Setup a server expecting XML with an encrypted assertion
  -  Run this php script [poc.php](https://github.com/user-attachments/files/24426600/poc.php.txt) with php -S 127.0.0.1:8888 (taken from [this saml test case](https://github.com/robrichards/xmlseclibs/blob/69fd63080bc47a8d51bc101c30b7cb756862d1d6/tests/saml/saml-decrypt.phpt#L62))

  - The script expects this private key: [sp-private-key.pem.](https://github.com/user-attachments/files/24426620/sp-private-key.pem.txt)

2. Create an XML document with an encrypted assertion (encrypted with aes-256-gcm)

Here is the SAMLResponse used in the video below: