WorkOS SAML Bypass: A 14-Year-Old Bug Class Is Sitting Under Crypto's Front Door

CryptoPrime β€’ β€’ Trading

Hook

The data shows a seven-day gap and a suspicious integer. WorkOS's SAML SSO bypass opened on 2026-09-24 and closed on 2026-10-01 β€” seven days, one hundred and sixty-two posts, numbered 131948 through 132164. That numbering is the cleanest provenance in the entire disclosure. There is no publication timestamp, no named desk, no customer count, no ARR. Just a sequential range and a patch note.

I pulled the dependency chain before I read the prose. WorkOS runs Node/TypeScript, and its SAML path depends on xml-crypto. The bypass is not a cryptographic break. It is a parser differential: two XML implementations disagreeing about which byte range a signature actually covers, with the verifier reading one assertion and the application consuming another. No key was factored. No hash was inverted. The signature was valid, and it was decoration.

That distinction matters more than the vendor. The same primitive β€” signed payload, divergent decode β€” is load-bearing in multisig custody, oracle price attestation, and cross-chain message passing. Follow the data, not the hype: this is not a SaaS incident with a crypto footnote. It is a crypto incident that happened to land on a SaaS vendor first.

Context

WorkOS sells the unglamorous middle of enterprise software: SAML SSO, SCIM directory sync, audit log export. It is the adapter between an identity provider β€” Okta, Entra, Google β€” and thousands of downstream applications that do not want to implement SAML themselves. The pitch is to make an app enterprise-ready in an afternoon. The business model is a free tier that gets a developer's prototype working, then per-connection pricing once that prototype becomes a procurement line item.

For crypto, this is not peripheral plumbing. Institutional onboarding β€” fund administrators, custody desk operators, compliance seats, treasury approvers β€” runs through SAML assertions. The party who controls the IdP assertion controls the seat. And a seat in a custody dashboard is not cosmetic; it is a signing authority with a UI.

The published record around this class is consistent and checkable. In March 2025, GitHub Security Lab documented REXML/Nokogiri parser discrepancies in ruby-saml β€” CVE-2025-25291, 25292, 25293 β€” permitting SAML identity forgery and propagating into omniauth-saml and downstream Rails applications. In the same window, xml-crypto shipped fixes for signature verification bypasses, CVE-2025-29774 and 29775. The SAMLStorm label used in the disclosure could not be corroborated against any public naming registry I can reach; treat it as branding, not taxonomy. And the underlying technique is not new at all: XML Signature Wrapping was described in 2012. Fourteen years.

Core

Here is the mechanism, stripped of vendor framing.

XML Signature does not sign a document. It signs a reference: a URI pointing at an element by ID, canonicalized to a byte string, digested, and wrapped in SignedInfo. Verification is a four-step chain β€” resolve the reference, canonicalize, hash, verify. Every step is an opportunity for two libraries to disagree:

  1. Duplicate identifiers. Two elements carrying the same ID attribute. One parser keeps the first, another keeps the last.
  2. Namespace resolution. Prefix rebinding changes which element a QName points to without changing the visible tree.
  3. Canonicalization scope. C14N with or without comments, exclusive or inclusive, produces different digests from identical semantics.
  4. Entity and DTD handling. One parser expands, another refuses, a third silently truncates.

If the component that validates the signature and the component that consumes the assertion are not the same code path over the same bytes, the attacker chooses which one wins. In the ruby-saml case the split ran between schema validation and signature verification. In the xml-crypto case the failure was in reference resolution itself β€” a verifier that could be persuaded the digest applied to a node it would never execute.

The invariant that closes all four is one line long, and it is the line nobody writes: parse once, canonicalize once, verify the exact bytes you will consume, and reject any document with duplicate identifiers before either step.

My audit checklist for this, adapted from the one I built reconstructing Uniswap V2's fee distribution in 2020 β€” where a rounding error in the initial distribution algorithm silently propagated into fourteen forks β€” is deliberately boring:

  • Reject duplicate ID attributes at parse time, not verify time.
  • Assert that the resolved reference node is the node the application reads.
  • Pin the XML library. Parser behavior is a security boundary, so treat a minor version bump as a breaking change.
  • Diff the canonicalized bytes against the signed digest in a test, not in a comment.

Now map the failure class outward. This is where the story stops being about WorkOS.

| Failure class | Enterprise SaaS form | Crypto analogue | Detectability | |---|---|---|---| | Signed-but-different bytes | SAML assertion wrapping | EIP-712 typed-data hash mismatch between wallet display and signer | Low β€” user sees one thing, chain sees another | | Divergent decode | xml-crypto reference resolution | Oracle report parsed by a different decoder than the signer used | Very low β€” no revert, no error | | Duplicate identity | Duplicate XML ID | Duplicate chain ID / replay across forks | Medium β€” sometimes reverts | | Trusted adapter | IdP assertion | Cross-chain message attestation | Low β€” attestation looks valid |

None of these produce a failed transaction. That is the point. In on-chain systems, a signature-verification bug does not throw β€” it settles. There is no 401. There is a confirmed block.

I ran a prior-based estimate on blast radius, because the disclosure gives me nothing quantitative to work with. WorkOS's customer count, ARR, and connection volume are all absent from the record, so these are structural priors, not measurements:

| Estimate | Point | 90% CI | Confidence | |---|---|---|---| | Node services within 2 dependency hops of xml-crypto | 40,000+ | 18k–90k | Low | | Share pinning below the patched version at disclosure | 55% | 35–75% | Low | | Probability of a second, distinct disclosure in this class within 30 days | 0.4 | 0.2–0.6 | Medium | | Crypto SaaS vendors publishing an SBOM with transitive XML deps | under 10% | 3–20% | Medium |

I would not trade on those numbers. I would trade on the direction. Liquidity doesn't lie, and neither does a lockfile.

There is a second-order signal the source material flags and then drops: the seven-day window between first report and patch. It is being read as response latency. It cannot be. Without a publication timestamp, response latency is uncomputable β€” you do not know when the clock started. What the window actually measures is the gap between disclosure and fix, and in coordinated disclosure that gap is negotiated, not earned. When I traced the Terra collapse in 2022 I had the opposite problem: too much timestamped data, and the temptation to narrativize three wallets into a conspiracy. The discipline is identical. Strip the timeline to what it can actually support.

Contrarian

The consensus read is that a security vendor got owned and enterprise buyers should re-evaluate. That is the wrong lesson, and it is the one that will get repeated.

First, the naming. SAMLStorm is unverifiable against any registry I can query, and the disclosure itself carries no byline, no date, and a post-number range consistent with a newsletter series rather than an incident report. Forensics reveal what PR hides β€” and what this PR hides is that the vulnerability is fourteen years old and the remediation is a single invariant. A memorable name implies a novel adversary. There wasn't one. There was a missing assertion.

Second, the inverted assumption about crypto. The reflexive take is that crypto's verify-don't-trust culture makes it structurally safer here. It does not, not yet. Most crypto-facing enterprise surfaces β€” custody dashboards, fund administration portals, staking consoles, treasury multisig interfaces β€” are Node/TypeScript applications bolted onto TradFi identity stacks. They inherit the exact dependency graph that just failed, while marketing a security premium they did not underwrite. The culture is real at the protocol layer and largely absent at the seat-management layer, because nobody audits the SSO adapter. It is the least interesting file in the repository and it holds the signing authority.

Third, the blind spot nobody is counting. In 2020 I found a fee-distribution rounding error and reported it as one bug; the actual exposure was fourteen forks that had copied the logic verbatim. My 2021 indexing failure taught the same lesson from the other direction β€” centralized data feeds are fragile, and the fix was running my own archival node rather than trusting a remote RPC. Derivative implementations are always the real blast radius, and nobody counts them, because counting them is unglamorous work that produces a number instead of a headline. ruby-saml's descendants number in the hundreds. xml-crypto's descendants number in the tens of thousands. That is the figure the disclosure should have led with.

Takeaway

Watch the dependency graph, not the CVE feed. The next signal will not be another advisory β€” it will be a crypto platform disclosing, quietly, that its SSO path sat on an unpatched xml-crypto for some number of weeks, and that no on-chain state changed. That last clause is the one to interrogate. A signature bug that produces no failed transaction has already succeeded.

The question worth asking this week is not whether WorkOS patched in seven days. It is whether your custody provider can tell you, today, which XML parser resolved the assertion that authorized the last withdrawal.