SAML Is Broken at Every Zoom Level
Trail of Bits calls SAML a fractal of bad design. The worst layer: signature wrapping — trusting XML the signature never covered.
Trail of Bits just published "SAML: A Fractal of Bad Design," and every engineer who has wired enterprise SSO is nodding grimly right now. The headline sounds like venting. It's actually a diagnosis: the bug classes in SAML integrations are the spec's design leaking through whatever library you put on top.
The most dangerous of those classes is old, well-documented, and still works in 2026: XML Signature Wrapping.
Why this matters
If you sell B2B software, your biggest customers demand SAML. Okta, Entra ID, and Ping all speak it, and "OIDC only" loses deals. So you're running an XML signature stack in your auth path whether you like it or not.
The failure mode is silent. A wrapped assertion doesn't throw an error. The signature verifies, your code reads the attacker's element, and someone logs in as your customer's CFO.
How it works
Signature wrapping
An XML signature doesn't vouch for the whole document. It covers elements by reference — a URI pointing at an element ID. Attackers duplicate the signed assertion, park the original inside a harmless wrapper, and edit the copy your code reads. Signature check passes; your app consumes forged attributes. Somorovsky's team documented this in 2014, and implementations kept falling for variants for years.
Canonicalization
Before signing, XML goes through canonicalization (C14N). Two documents that render identically in a browser can canonicalize differently, so "what you see" and "what was signed" quietly diverge. That's a whole class of parser differentials baked into the format.
Field validation
Even a perfect signature says nothing about semantics. Comment injection like admin@evil.com<!--@real.com--> defeated XPath-based NameID extraction in OneLogin's python-saml (CVE-2017-11427). Audience, Destination, and InResponseTo checks are all on you.
Where this helps
- Enterprise SSO endpoints: confirm your library ties the signature's reference to the exact assertion it consumes — not merely that "a signature exists."
- Multi-tenant apps: Audience must be a per-tenant entity ID. A shared value means tenant A's token opens tenant B's door.
- Security reviews: XSW is among the first things a competent pentester fires at you. Finding it first is cheaper.
- Roadmap arguments: this is your evidence for steering every new integration toward OIDC.
Watch out
You can't patch the spec. Even mature implementations — Shibboleth, python-saml — have shipped CVEs, so pin versions and watch advisories.
Never relax validation to make a customer's broken IdP metadata work. That's how InResponseTo checks quietly disappear from your codebase.
And you probably can't drop SAML entirely; governments and legacy enterprises still require it. Compensate with strict validation, short assertion windows, and allowlisted IdPs.
Try it yourself
Decode a response from your test IdP and check what the signature actually covers:
import base64
from lxml import etree
# Save the base64 SAMLResponse value from your IdP's POST
doc = etree.fromstring(base64.b64decode(open("response.b64", "rb").read()))
ref = doc.find(".//{http://www.w3.org/2000/09/xmldsig#}Reference")
print("Signature covers:", ref.get("URI"))
a = doc.find(".//{urn:oasis:names:tc:SAML:2.0:assertion}Assertion")
print("App consumes: ", "#" + a.get("ID"))If those two lines don't match — or your library can't even tell you — you found the hole before someone else did.
TL;DR
- What changed: Trail of Bits published a systemic teardown of SAML — flawed at every layer, not just in implementations.
- Why it matters: signature wrapping and lazy assertion checks silently log attackers in as real users, and B2B software can't drop SAML.
- Try today: decode one SAML response and verify the signed Reference URI matches the assertion ID your app consumes.