GHSA-cw72-f8pq-h4pg · CWE-345 · xmlseclibs 4.0.0 · fixed in 4.0.1 · High severity, CVSS 7.4
My report, published as GHSA-cw72-f8pq-h4pg, showed that the RSA-to-HMAC key-confusion protection introduced in xmlseclibs 4.0.0 was incomplete. The release rejected obvious PEM-encoded public keys and certificates, but it still accepted the same public material when supplied in a binary representation such as DER.
In plain terms, an RSA certificate is meant to verify signatures made with a private key. In the affected flow, the library instead treats the certificate's public bytes as a shared HMAC secret. Because those bytes are public, an attacker can calculate a valid signature without ever knowing the RSA private key.
Finding at a glance
| Property | Value |
|---|---|
| Advisory | GHSA-cw72-f8pq-h4pg |
| Component | robrichards/xmlseclibs |
| Affected version | 4.0.0 |
| Patched version | 4.0.1 |
| Weakness | CWE-345 insufficient verification of data authenticity |
| Attack primitive | Attacker-selected hmac-sha1 signature method combined with a trusted public key in DER or another non-PEM format |
| Impact | Forgery of signed XML without the RSA private key |
| Severity | High — 7.4, CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N |
| Reporter | jf0x3f |
How the two advisories compare
| Question | GHSA-m5mw-mr39-66vp | GHSA-cw72-f8pq-h4pg |
|---|---|---|
| What was found? | An attacker could substitute hmac-sha1 for an expected RSA signature and make the relying party use public RSA material as the HMAC secret. | The 4.0.0 fix still allowed that attack when the trusted public material was encoded as DER or another unrecognized non-PEM representation. |
| Affected versions | < 4.0.0 | = 4.0.0 |
| Patched version | 4.0.0 | 4.0.1 |
| Remediation | Bind SignatureMethod to the key object's algorithm; reject certificates identified with $isCert and values containing PEM armor; add the safer verifyDocument() path. | Recognize asymmetric key material from its parsed content and structure, independent of PEM, DER, bare base64, or other supported encodings; enforce the guard while loading, signing, and verifying. |
| Why 4.0.0 was bypassable | The intended invariant was broader than the implemented representation checks. | DER contains neither the caller-supplied certificate flag nor the literal -----BEGIN marker, even though it carries the same public key. |
| Published severity | Critical | High, CVSS 7.4 |
| Reporter | offset | jf0x3f |
What the original advisory fixed
GHSA-m5mw-mr39-66vp identified the original algorithm-substitution flaw. Before 4.0.0, verify() did not always require the XML document's SignatureMethod to match the algorithm of the supplied key. An attacker could replace an expected RSA method with hmac-sha1 and turn public RSA material into an attacker-known HMAC secret.
Version 4.0.0 added two relevant defenses:
verify()rejects a key object whose declared type differs from the document'sSignatureMethod.loadKey()rejects an HMAC key when the caller sets$isCert = trueor when the input contains the PEM marker-----BEGIN.verifySignature()repeats the PEM-marker check as defense in depth.
if ($objKey->type !== $sigMethod) {
throw new Exception(
'SignatureMethod algorithm does not match the supplied key type'
);
}
Those changes stopped the direct PEM-based proof and made verifyDocument() a safe-by-default alternative with a caller-supplied pinned key and algorithm allowlists.
The key idea
RSA and HMAC use keys in fundamentally different ways:
- RSA verification uses a public key. Anyone may know it, but only the private-key holder should be able to create a valid signature.
- HMAC verification uses a shared secret. Anyone who knows that secret can create a valid MAC.
A public certificate is safe to distribute when it remains an RSA verification key. It becomes unsafe when the verifier mistakes its bytes for an HMAC secret, because every attacker who can obtain the certificate now knows the supposed secret.
Why the 4.0.0 fix was incomplete
The intended security rule was: asymmetric key material must never become an HMAC secret. The implemented 4.0.0 rule was narrower: reject the value if the caller labels it as a certificate or if its bytes contain a PEM header.
Those are useful signals, but they describe a representation—not the key's actual meaning. PEM and DER can encode the same certificate. PEM adds readable armor around base64 data; DER is the underlying binary ASN.1 structure and contains no -----BEGIN text.
XMLSecurityKey::loadKey() rejects asymmetric material for HMAC_SHA1 only when the caller sets $isCert or the supplied data contains a PEM header:
if (($this->cryptParams['library'] ?? null) === self::HMAC_SHA1) {
if ($isCert) {
throw new Exception('An X.509 certificate cannot be used as an HMAC key');
}
if (strpos($this->key, '-----BEGIN') !== false) {
throw new Exception('Asymmetric key material cannot be used as an HMAC key');
}
}
This catches a certificate explicitly identified by the caller and text-based PEM input containing -----BEGIN. It does not determine the key's real type.
DER encodes the same certificate or public key as binary bytes, so it has no PEM marker. DER certificates, DER SubjectPublicKeyInfo keys, and other non-PEM public-key representations can therefore pass the check and be accepted as HMAC secrets.
Why the new algorithm check did not stop the bypass
The 4.0.0 algorithm-binding check compares the XML method with the type of the key object. In the low-level flow, however, locateKey() creates that object from the XML document itself:
$algorithm = $xpath->evaluate($query, $node);
$objKey = new XMLSecurityKey($algorithm, array('type' => 'public'));
If the attacker declares hmac-sha1, locateKey() returns an HMAC object. Later, verify() compares that HMAC object with the same attacker-selected hmac-sha1 value, so the types match exactly. The application then loads its trusted DER certificate bytes into that already-HMAC-typed object.
The check therefore proves only that the object's label agrees with the XML. It does not prove that the bytes stored inside the object are a legitimate shared secret. Because DER bypasses the format check, both 4.0.0 defenses pass while the original RSA-to-HMAC confusion remains possible.
How the attack reaches verification
- The attacker supplies XML whose
SignatureMethoddeclareshmac-sha1. XMLSecurityDSig::locateKey()uses that document-selected algorithm and returns an HMAC key object.- The application loads its trusted RSA certificate or public key into that object as DER bytes.
loadKey()sees neither$isCert = truenor a-----BEGINmarker, so it accepts the public bytes as the HMAC secret.- The library verifies the signature with those same public bytes.
- The attacker's forged XML is accepted even though the RSA private key was never used.
Attacker-controlled XML
-> SignatureMethod = hmac-sha1
-> locateKey() creates an HMAC key
-> application loads trusted public DER bytes
-> loadKey() misses the asymmetric key type
-> verify() accepts an attacker-computable HMAC
The vulnerable behavior comes from combining two trust decisions: the XML chooses the algorithm, while the application supplies the trusted key material. The verifier does not enforce that the key's actual type matches the RSA algorithm the application intended to trust.
Conditions required for exploitation
An integration is affected when all of the following are true:
- it uses the low-level
locateKey()verification flow; - the untrusted XML can select
hmac-sha1; - the application loads a trusted asymmetric public key or certificate into the located key object;
- the key material is supplied as DER or another non-PEM representation; and
- the caller does not pass
$isCert = true.
These integration requirements are why the published CVSS vector uses High attack complexity. Applications that construct and pin an RSA verification key themselves, instead of accepting the algorithm selected by the XML, avoid this specific path.
Affected code path
src/XMLSecurityDSig.php:1124-1150—locateKey()src/XMLSecurityKey.php:424-450—loadKey()src/XMLSecurityDSig.php:1163-1208—verify()src/XMLSecurityKey.php:854-887—verifySignature()
Security impact
In an affected integration, an unauthenticated remote attacker can forge signed XML and impersonate trusted assertions, identities, or roles without possessing the RSA private key. If the application uses signed XML for authentication or authorization decisions, the attacker may be able to change authenticated data or obtain privileges assigned to a trusted sender.
The demonstrated attack can affect confidentiality and integrity. It does not directly affect availability.
The published severity is High with CVSS 3.1 score 7.4. Attack Complexity is High because exploitation depends on the consuming application using the vulnerable low-level loading pattern; the attacker cannot choose that application behavior.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N
How 4.0.1 completes the fix
Version 4.0.1, released in commit 3d910b23ac1f566c2fa48abf795d710f78a3b770, replaces the representation-dependent decision with an isAsymmetricKeyMaterial() check:
if (is_string($this->key)
&& self::isAsymmetricKeyMaterial($this->key)) {
throw new Exception(
'Asymmetric key material cannot be used as an HMAC key'
);
}
The new guard recognizes what the bytes contain rather than relying on how they are wrapped. The implementation:
- keeps the PEM marker as a fast rejection path;
- uses phpseclib key and X.509 parsing for PEM, DER, bare base64, and other supported representations;
- recognizes ASN.1 SubjectPublicKeyInfo and PKCS#8 key containers by structure, including algorithms phpseclib cannot instantiate;
- recognizes encrypted PKCS#8, traditional encrypted PEM, OpenSSH private keys, and PuTTY key files; and
- applies the same check in
loadKey(),signData(), andverifySignature(), so direct assignment cannot skip the defense.
The 4.0.1 regression tests cover DER and headerless-base64 certificates and public keys, unsupported asymmetric algorithms, direct assignment, and valid binary or base64 HMAC secrets. That last negative control matters: the fix rejects actual asymmetric structures without banning arbitrary non-text HMAC secrets.
This completes the original remediation because the enforced invariant now matches the intended one: asymmetric material is rejected as an HMAC secret regardless of serialization format.