Authentication bypass to admin in Drupal's SAML SSO module
Drupal's SAML SSO - Service Provider module
(miniorange_saml) logs a user in from a signed assertion handed over by a
trusted identity provider. The catch: it reads the signature algorithm
from the attacker's assertion instead of pinning it to the IdP's key.
Flip that algorithm from RSA to HMAC and the module verifies the forgery using the IdP's
public certificate as the signing secret, material that is public by
design. An unauthenticated attacker forges an assertion for any account, including an
administrator, and is logged straight in. No password, no access to the IdP.
Anonymous → administrator in one run
The proof of concept holds no account and never touches the identity provider. It builds
an assertion for the admin user, signs it with HMAC keyed on the IdP's
public certificate, and posts it to the SAML login endpoint. What comes back is a
Drupal administrator session.
Replay of saml_takeover_poc.py. Reproduced end-to-end in a lab against
miniorange_saml 3.1.4 on Drupal 11.
Four steps, nothing secret required
Every input the attacker needs is public: the IdP certificate, the entity IDs, the login route, and the username to impersonate. The only privileged actor is the Service Provider itself, trusting a signature it should have rejected.
Take the cert
The IdP's x509 certificate is published in the SP config and metadata. Grab it.
Forge the claim
Build a SAML assertion naming any user: here, admin.
Switch to HMAC
Sign with HMAC-SHA1, keyed on the public cert the SP already trusts.
Accepted as admin
The SP verifies the HMAC with that same cert and logs you in.
The signature is the whole trust boundary
In SAML single sign-on, a site (the Service Provider, or SP) outsources login to an identity provider (IdP). The IdP authenticates the user and hands back a signed XML assertion: "this is alice, she logged in at 09:14". The SP does not re-check the password. It only checks one thing before it trusts that claim and creates a session: is the signature valid for the IdP we configured? That signature is the entire security boundary.
How that check is done depends on the signing scheme, and the two common schemes are not interchangeable:
- RSA (asymmetric). The IdP signs with a private key; the SP verifies with the matching public key. The public key is meant to be published, and that is safe, because it cannot produce signatures.
- HMAC (symmetric). One shared secret both signs and verifies. Anyone who holds that secret can forge a valid signature.
RS256 → HS256
algorithm-confusion bug.
The verifier trusts the attacker's algorithm
When the SP validates an assertion, it needs a key object for the signature check. The module builds that key from the algorithm named inside the incoming assertion, then loads the configured IdP certificate into it. There is no step that binds the algorithm to the key's actual type:
// src/Utilities.php -- castKey() takes the algorithm from the assertion $algorithm = $signatureMethod; # read from <ds:SignatureMethod>, attacker-controlled $key = new XMLSecurityKey($algorithm); # RSA? HMAC? whatever was sent $key->loadKey($idpCertificate); # the PUBLIC cert, used as key material
Send an RSA-signed assertion and loadKey() treats the certificate as a public
key, which is correct. Send an HMAC assertion and the exact same
certificate bytes are treated as the shared secret. Because that
certificate is public, the attacker computes the HMAC themselves, and the signature
verifies. A forged assertion for any NameID, including an administrator, now
passes the one check that stood between an anonymous request and a session.
What the session is worth
The forged login lands as a full Drupal administrator. From there the site is effectively owned: create and elevate accounts, change any content or configuration, read the data the site holds, and in a typical deployment reach code execution through administrative features. The authentication bypass is the whole ballgame; everything after it is ordinary admin.
Scoring & scope
Drupal contrib advisories use the Drupal Security Team's 25-point risk model rather than a
CVSS vector. SA-CONTRIB-2026-145 is rated
Moderately critical, 14/25
(AC:Complex/A:None/CI:Some/II:Some/E:Theoretical/TD:Default):
The metric that matters most to a defender is Authentication: None. The attack runs with zero credentials on the Drupal site and no access to the identity provider, and every input it needs (the IdP certificate, entity IDs, the ACS route, the target username) is public metadata or a fixed route. The bug reaches any site running the module with SAML login enabled.
| Branch | Affected | Fixed | Status |
|---|---|---|---|
| 3.x | < 3.2.0 | 3.2.0 | patch |
Fixing & detecting it
- Update to 3.2.0 (
composer require drupal/miniorange_saml:^3.2.0). The fix adds an explicit algorithm allow-list: the SP now accepts only RSA signature methods and rejects anything else before it ever loads the key, so an HMAC assertion is thrown out instead of verified. - Pin the algorithm, always. The general lesson for any SAML or JWT verifier: bind the accepted signature algorithm to the configured key type. Never let the incoming message decide how it gets checked.
- Detection: look in request logs for
SAMLResponsebodies whose<ds:SignatureMethod>is an HMAC algorithm (...#hmac-sha1and friends) when your IdP signs with RSA; and for logins attributed to privileged accounts that were not preceded by a redirect out to the IdP.
Timeline
- 2026-06-26Reported to the Drupal Security Team through the private process, with a reproducing proof of concept.
- 2026-07Maintainers develop the fix (an RSA-only algorithm allow-list in
castKey()); I review and verify it in the lab. - 2026-09-14Advisory SA-CONTRIB-2026-145 published, 3.2.0 released, CVE-2026-87947 assigned.
Coordinated disclosure through the Drupal Security Team, the authorized channel for contrib modules. Details are released only after a fixed build was available.