DEV Community

Cover image for How SAML works, and why XML signature wrapping keeps breaking it

How SAML works, and why XML signature wrapping keeps breaking it

SAML is the XML that logs you into almost every work app you use, and it has a design quirk that keeps producing the same security bug: the signature lives inside the document it signs. In 2012 researchers tested fourteen SAML frameworks and broke eleven with one trick, XML signature wrapping. Last week a Trail of Bits post, "SAML: A fractal of bad design", argued it is time to retire the protocol. If you sell software to companies, somebody will ask you for SAML, so here is how it works and where it breaks.

TL;DR

  • SAML (Security Assertion Markup Language) dates from 2002. An OASIS committee merged four vendor XML formats into one, and a whole single sign-on industry grew on top of it.
  • A SAML login is a signed XML "assertion" that says who you are. It travels from the identity provider to the app through your browser, so the user carries the proof.
  • The signature is enveloped: it sits inside the assertion. To check it, the app has to cut it back out and rebuild the exact bytes that were signed. One byte of disagreement and nobody logs in.
  • The bug class that keeps coming back: the code that verifies the signature and the code that reads the username look at different parts of the document. That split broke 11 of 14 frameworks in 2012, many libraries again in 2018 and ruby-saml in 2025.
  • Trail of Bits' advice for new services: "Just support OIDC. Abandon SAML." If a customer forces it on you, use a maintained library and keep it patched.

A short history of SAML and single sign-on

SAML was created in 2002 by the OASIS Security Services Technical Committee. Per the Trail of Bits post, four contributed specs went in: S2ML from Netegrity, AuthXML from Securant, X-TASS from VeriSign and ITML from Jamcracker. Four XML security formats, one committee, one standard. Matt Schwager, the author, calls that "a recipe for 'kitchen-sink' protocol design".

Universities adopted it first: CAS at Yale in 2002, Shibboleth from the Internet2 consortium in 2003, Microsoft's ADFS the same year and simpleSAMLphp around 2007. Then the companies came. Ping Identity in 2002, OneLogin and Okta in 2009, Duo in 2010 with its first SSO product in 2015. Schwager worked on that Duo product. Most of those companies, in his words, "were essentially built on the SAML protocol". A committee document turned into a multibillion-dollar industry, which is a faster conversion than most committee documents get.

How does SAML authentication work?

The common flow is "SP-initiated Web SSO". The app you want is the service provider (SP). The place that knows your password is the identity provider (IdP), say your company's Okta.

Step What happens
1 You open the app. It has no session for you.
2 The app redirects your browser to the IdP with an AuthnRequest.
3 You log in at the IdP (password, MFA, whatever it enforces).
4 The IdP gives your browser a signed XML Response containing an Assertion, usually as an auto-submitting form (the HTTP-POST binding).
5 The browser posts it to the app. The app verifies the XML signature and reads the user from Subject/NameID.

The important detail is step 4. The app and the IdP never talk directly. Everything the app needs rides inside one document that passes through the browser, which is to say through the user, the one party you should not trust with it. The signature makes that safe, if the check is reliable.

Where the SAML signature lives: enveloped XML signatures

Here is a simplified assertion, the same one I used in the video. The real thing has more namespaces, conditions and timestamps:

<!-- simplified sketch of a SAML assertion -->
<saml:Assertion ID="_a75ad">
  <saml:Issuer>https://idp.example.com</saml:Issuer>
  <ds:Signature>
    <ds:SignedInfo>
      <ds:Reference URI="#_a75ad">…</ds:Reference>
    </ds:SignedInfo>
    <ds:SignatureValue>…</ds:SignatureValue>
  </ds:Signature>
  <saml:Subject><saml:NameID>niko@corp.example</saml:NameID></saml:Subject>
</saml:Assertion>
Enter fullscreen mode Exit fullscreen mode

The ds:Signature element sits inside the saml:Assertion it signs, and it finds its target by ID: Reference URI="#_a75ad" points back at the assertion's ID attribute. That is an enveloped signature. It is like a notary who staples his stamp inside the envelope he is sealing.

To check it, the app has to undo the stapling. It removes the Signature element (the "enveloped-signature" transform), then canonicalizes what is left, and hashes the result. Canonicalization, C14N for short, normalizes the XML so that two documents that mean the same thing produce the same bytes: whitespace, attribute order, namespace declarations. Schwager's summary: "if the SP and IdP cannot agree on a consistent representation of the XML data, then the bytes won't line up, the signatures won't match, and your authentication fails."

Compare a JSON Web Token, the format OpenID Connect uses:

base64url(header).base64url(payload).base64url(signature)
Enter fullscreen mode Exit fullscreen mode

The signature is detached, three parts separated by dots. You verify the bytes you received. There is nothing to cut out first and nothing to normalize. As the Trail of Bits post puts it, "it is very difficult to get a byte-for-byte, canonically equivalent representation of the data when you're also modifying it!"

And before any of that, a SAML library has to survive XML itself: XXE, "billion laughs" entity expansion and the rest. Thomas Ptacek, quoted in the post from 2023: SAML "works … if you assume XML signature validation is reliable. But XML signature validation is deeply cursed".

What is XML signature wrapping?

Here is the crack. In most SAML libraries, verifying the signature and reading the username are two separate steps, often in two separate pieces of code:

  1. The verifier follows Reference URI="#…" to an element by ID, checks the hash and signature, and says "valid".
  2. The reader then goes looking for "the assertion" and "the NameID" in the document, usually by position or by an XPath.

Nothing forces those two steps to land on the same element. XML signature wrapping (XSW) exploits that gap: the attacker takes a genuinely signed assertion, moves it somewhere the reader ignores, and puts a second, unsigned assertion where the reader looks. The verifier finds the original by its ID and approves it. The reader finds the forgery and logs the attacker in as whoever it names.

Looks at Sees
Signature verifier the element with the referenced ID the original, correctly signed
Username reader "the first assertion", by position the attacker's copy

The paper that made this famous is "On Breaking SAML: Be Whoever You Want to Be" by Somorovsky and colleagues at USENIX Security 2012. They ran "an in-depth analysis of 14 major SAML frameworks and show that 11 of them, including Salesforce, Shibboleth, and IBM XS40, have critical XML Signature wrapping (XSW) vulnerabilities."

Chart: of 14 SAML frameworks tested in 2012, 11 had XML signature wrapping vulnerabilities

Schwager calls it "the godfather of it all". His team at Duo picked simpleSAMLphp as its base because, in his words, it was resilient to XSW "at a time when nobody really knew what that was".

Same bug, new decade: the 2018 comment bug and ruby-saml in 2025

The details change. The shape does not: two pieces of code disagree about what the document says.

2018, one comment. Kelby Ludwig at Duo Labs found that canonicalization strips XML comments, so adding one does not invalidate the signature. But some XML APIs, when you ask for an element's text, stop at the first text node. Duo's toy payload, verbatim from the post:

<NameID>user@user.com<!---->.evil.com</NameID>
Enter fullscreen mode Exit fullscreen mode

The signature covers user@user.com.evil.com, an account the attacker legitimately owns. The app reads user@user.com. As Ludwig wrote, "a simple seven-character addition". Duo's list of affected software included OneLogin's python-saml (CVE-2017-11427) and ruby-saml, Clever's saml2-js, OmniAuth-SAML, Shibboleth and Duo's own gateway (CERT VU#475445).

2025, two parsers. Peter Stöckli at GitHub Security Lab found that "ruby-saml was using two different XML parsers during the code path of signature verification. Namely, REXML and Nokogiri." When two parsers read the same bytes differently, the verifier and the reader are again looking at two different documents. The result was CVE-2025-25291 and CVE-2025-25292 in ruby-saml up to 1.17.0: attackers "in possession of a single valid signature" from the target's IdP could "construct SAML assertions themselves" and log in as any user. GitHub found an exploitable instance in GitLab. The fix is ruby-saml 1.18.0, and anything that depends on it, such as omniauth-saml, needs updating too.

The Trail of Bits post lists three more parser-differential and round-trip write-ups from 2025 alone, one titled "SAML roulette: the hacker always wins". Thirteen years after USENIX, the bug class is still producing new research.

Why SAML is "a fractal of bad design"

The argument in the title: every level you zoom into has the same flaw. Schwager lists five:

  • Built on XML. Tags, attributes, namespaces, comments, CDATA, DOCTYPEs, schemas. JSON has keys, values, objects and lists.
  • Canonicalization. You cannot hash XML without first agreeing on what the bytes are.
  • Enveloped signatures. You modify the thing you are verifying.
  • Kitchen-sink design. "Any given SAML authentication you would encounter in the wild today probably avoids 90% of the specification." The other 90 % is still code somebody ships.
  • Ossification. SAML was built for a world of VPNs and firewalls where the IdP and SP could not talk to each other. OIDC assumes HTTPS and a back channel, and it grew spec by spec: PKCE, device flow, DPoP.

The Hacker News thread (312 points, 161 comments) mostly agreed, and then the buyer showed up. The top-level reply from ocdtrekkie: "Eh, if you don't have SAML support, I can find a product that does." Both are true, and that is the whole problem. SAML is bad to implement and mandatory to sell.

SAML vs OIDC: what to do on Monday

I am not a security auditor. These are the practical rules from the sources above.

If you are building a new service: ship OpenID Connect first. Ptacek, quoted in the post from 2024: "We've managed to hold the line on OIDC so far. So has Tailscale. If Tailscale can hold the line, given who they're selling to, I think most orgs can." Fly.io and Tailscale sell to enterprises without SAML.

If a customer forces SAML on you:

  • Use a maintained library and keep it patched. The 2018 and 2025 bugs shipped as fixed releases, so an unpatched install is the real exposure. Check transitive dependencies too (the omniauth-saml case).
  • Never write your own XML signature check. Per Ptacek, "most fielded SAML implementations are wrapping libxmlsec", and that is the part you least want to reinvent.
  • Read the user only from the element the signature actually covered, never from "the first assertion in the document".
  • Be strict about shape. Ptacek again, from 2021: consider "rejecting any message that doesn't have the same shape as what Okta, Onelogin, Google, or Shib generates."

If you run an identity provider: Schwager's migration plan is the boring one. Stop onboarding new SAML integrations, give existing customers an equivalent OIDC configuration, set a sunset date.

Verdict: REVERT

I stamped this one REVERT. Almost twenty-five years, one bug class that never dies, and the fix everyone agrees on is a different protocol. SAML earned its place: it built the single sign-on industry and secured an enormous number of logins. But a signature you have to cut out of the document before you can check it is a design that asks every library author to be perfect, and the record since 2012 says they are not. In the previous Under the Hood I took apart passkeys and stamped them NEEDS REVIEW. SAML gets the harsher stamp.

FAQ

What is SAML used for?
Single sign-on for web apps, mostly at companies and universities. You log in once at an identity provider such as Okta, ADFS or Shibboleth, and it vouches for you to each app with a signed XML assertion.

What is XML signature wrapping in SAML?
An attack where a validly signed assertion is moved to a place the app's username reader ignores, and a forged one is placed where it looks. The signature check passes on the original; the app logs in the forgery.

Is SAML still secure?
A maintained, patched library with strict input checks is the realistic standard. The protocol's design makes implementation bugs likely, which is why Trail of Bits recommends OIDC for anything new.

SAML vs OIDC: which should I implement?
OIDC first. Add SAML only when a customer requires it, and then through a maintained library, never your own signature code.

Sources


This article expands on an episode of **The Daily Diff, a five-minute daily video on what shipped and what broke in tech.
Watch the episode · Subscribe on YouTube · the written diff lands in your inbox every morning at thedailydiff.dev.

Top comments (0)