# xmlseclibs: Missing AES-GCM Authentication Tag Validation on Encrypted Nodes Allows for Unauthorized Decryption

xmlseclibs never validates AES-GCM auth tag length, letting an attacker brute-force a 1-byte tag, recover the GHASH key, and decrypt or forge encrypted XML nodes.

- Payout: Research
- Severity: high
- Program: xmlseclibs
- Found by: Sideni
- Disclosed: 2026-03-13
- CVE: CVE-2026-32313
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-4v26-v6cg-g6f9
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-4v26-v6cg-g6f9
- Tags: area:oss domain:oss impact:data-leak impact:integrity kind:research platform:ghsa sev:high tech:php technique:blind technique:source-review vuln:crypto

**Target:** PHP XML encryption library (XMLSecurityKey AES-GCM decrypt)
**Root cause:** The auth tag is extracted via substr() from the ciphertext blob without checking strlen($authTag) == AUTHTAG_LENGTH; OpenSSL only compares the given tag against the prefix of the real tag, so a 1-byte tag can be brute-forced.
**Steps:**
1. Base64-decode the <xenc:CipherValue> content; first 12 bytes are the nonce, last 16 bytes the tag.
2. Brute-force the tag of an empty ciphertext: loop all 256 byte values, concatenate nonce + byte_tag_attempt, base64-encode, replace <xenc:CipherValue>.
3. HTTP 500 responses reveal a valid tag byte; repeat per byte until all 16 tag bytes are recovered.
4. With the empty ciphertext and its tag, compute the GHASH key (per frereit.de/aes_gcm).
5. Use the GHASH key to compute auth tags offline for arbitrary ciphertexts.
6. Decrypt by observing XML parsing errors (HTTP 500) after modifying the ciphertext.
**How found:** Code reading of XMLSecurityKey.php decrypt path noticed $authTag set from substr() with no length check, then consulted PHP openssl_decrypt docs confirming tag length is caller's responsibility and only prefix-matched.
**Exploitation:** Attacker submits crafted SAMLResponse with truncated GCM tag to a server decrypting encrypted assertions; oracle via HTTP 500 distinguishes valid tag bytes, enabling byte-by-byte tag brute-force, GHASH key recovery, then offline decryption and forgery of arbitrary ciphertexts.
**Escalation:** Recovered GHASH key allows forging arbitrary authenticated ciphertexts and decrypting any AES-GCM encrypted node; static symmetric keys must be rotated.
**Insight:** When a library slices a fixed-size field (IV/tag) from a variable-length blob with substr(), a short blob yields a short field; combined with an error oracle this turns a 128-bit tag into a byte-by-byte brute-force. Check tag length validation in any AES-GCM wrapper.

## Proof of concept (copied exactly from the original report)

**PHP snippet showing short auth tag accepted**

```
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: ""
*/
```

**Brute-force tag steps for empty ciphertext**

```
3. Take the content of the `<xenc:CipherValue>` node and apply the following modifications
    1. Base64-decode the content
    2. Take the first 12 bytes and save them as the nonce
    3. Take the last 16 bytes and save them as the tag
    4. Now brute-force the tag of an empty ciphertext
        1. Loop through all 256 possible byte values (let's call that `byte_tag_attempt`)
        2. Concatenate together the nonce and the `byte_tag_attempt`
        3. Base64-encode the result
        4. Replace the content of the `<xenc:CipherValue>` node with this result
        5. On http errors 500, we learn that the tag is valid
        6. Do the same for the next byte of the tag until all 16 bytes have been brute-forced
```

