In this article
October 14, 2026
October 14, 2026

Our response to four SAML vulnerabilities, from report to remediation

In late September 2026, we fixed four SAML SSO authentication bypasses where signature verification and identity extraction could disagree on the same response. This post covers the root cause, our response, and guidance for developers who process signed XML.

Explore with AI
Open in ChatGPT
Open in Claude
Open in Perplexity

Overview

Between September 24, 2026 and September 27, 2026, WorkOS received two vulnerability reports from a security researcher, Alex Tan (ahacker1), describing issues that allowed a crafted SAML response to sign in as another user on a targeted single sign-on (SSO) connection.

An attacker needed a SAML response legitimately signed with the key WorkOS trusted for the targeted SSO connection. For some vulnerabilities, the publicly available signed identity-provider metadata could meet that requirement.

Our investigation and follow-up testing identified four vulnerabilities in how our implementation handled SAML responses.

We blocked the initially reported vulnerability approximately 1 hour and 40 minutes after receiving the report. Further testing uncovered additional ways to bypass authentication. By September 27, 2026 at 09:41 UTC, WorkOS deployed multiple controls that blocked all identified exploit variants. No action is required from customers to receive these fixes.

Our review of the relevant system logs identified no evidence of exploitation beyond security testing by the researcher and the customers who received the reports.

We are sharing the shortcomings identified in our implementation and the changes we made, along with guidance for developers who process signed XML.

This is separate from our earlier SAMLStorm (CVE-2025-29774 and CVE-2025-29775) disclosure last year, a flaw in xml-crypto that allowed attackers to forge SAML responses.

Background

Understanding these vulnerabilities requires some background on how a SAML sign-in works and how its signatures are checked.

SAML lets an identity provider (IdP), such as Okta, tell another service who a user is. The identity provider sends a SAML response, which contains an assertion about the user, such as their email address. It signs the response, the assertion, or both, so the recipient can verify that the signed content hasn't been altered.

In SAML terms, the service that receives and checks the response is the service provider. For applications that use WorkOS for SSO, WorkOS is the service provider, acting on behalf of the application.

Here's what happens when Alice, an employee at Globex, signs in to one of those applications:

The response reaches WorkOS through Alice's browser, as a form submission (step 7). WorkOS can't assume those bytes are what the identity provider produced, because anyone submitting a response can edit any part of it first. Only the signature can show which parts are genuine.

To accept a response, a service provider has to answer two questions:

  1. Is it signed? Was the signature made with a certificate WorkOS trusts for this organization's identity provider, and does it cover the content it claims to?
  2. What does it say? Who is the user, did the sign-in succeed, is the response meant for this application, and is it still valid?

In many SAML implementations, including WorkOS', different code answers each question. A signature library answers the first. The application answers the second by reading fields from its own parsed copy of the response.

Separating signature verification from application validation requires an explicit binding between authenticated content and the fields used to sign a user in. In this incident, the verifier and the application could disagree, allowing a signature to be associated with a forged identity.

Keeping the two views identical requires careful handling because of how XML and XML signatures work. The four points below explain why. Each one is the basis for at least one of the identified vulnerabilities.

There are many ways to write the same SAML document

SAML is an XML document. XML is a textual format for a tree. The text is just one of many ways to write that tree down. These documents each represent the same tree:

<note>1 &lt; 2</note>
<note>1 &#60; 2</note>
<note><![CDATA[1 < 2]]></note>

To sign a tree, the signer and verifier must agree on how to write it down. Canonicalization is that agreement: a set of rules that turns an XML tree into a deterministic byte string.

Signatures cover bytes while applications read trees

A cryptographic signature covers a sequence of bytes, not a tree. To check the signature on part of a SAML response, the verifier canonicalizes that part into bytes, hashes them, and compares the result with the value the identity provider signed.

The application never looks at those bytes. It decides who the user is, which application the assertion is for, and whether it's still valid by reading fields from a parsed tree.

So the signature only protects the application if the tree it reads is exactly the tree that produced the verified bytes. Any step that can build a different tree from the same input, or different bytes from the same tree, gives an attacker room to make the two disagree.

A signature sits inside what it signs, so it signs indirectly

In SAML, the signature is usually embedded inside the element it protects. A signature can't sign itself, so it works in two steps:

<Assertion ID="_a1">
  <Signature>
    <SignedInfo>
      <Reference URI="#_a1">                <!-- "the element with ID _a1" -->
        <DigestValue>q3Jc…</DigestValue>    <!-- hash of that element -->
      </Reference>
    </SignedInfo>
    <SignatureValue>Mv8x…</SignatureValue>  <!-- signs SignedInfo only -->
  </Signature>
  <Subject>alice@globex.example</Subject>
</Assertion>
  1. The identity provider's key signs only a small manifest, SignedInfo. The verifier checks SignatureValue against a certificate it already trusts for that connection.
  2. The manifest names the protected element by ID and records a hash of it. The verifier finds the element with that ID, removes the Signature element from it, hashes the rest, and compares the result with the recorded hash

‍SignedInfo contains the canonicalization method and each reference lists its transforms. Instructions are part of the signed structure, and the verifier must select and interpret that authenticated structure consistently.

Three consequences matter for this incident:

  • The assertion is protected only through the manifest. If the manifest can be read two different ways, the verifier and the application can disagree about which element is signed.
  • The manifest names an element by ID, not by location. The service provider has to make sure the element that was hashed is the element it reads.
  • Anything inside the Signature element is left out of the hash. Content hidden there isn't covered by the signature, even though it's still in the document.

Turning text into a tree and back isn't lossless

Software rarely keeps the original text of an XML document. It reads the text into a tree, works with the tree, and sometimes writes the tree back out as text to hand to another component. It's natural to assume that reading the new text gives back the same tree. It doesn't always, for two reasons.

Writing a tree back out can create new elements. When a tree holds text that looks like markup, such as <b>, the software writing it out has to escape it as &lt;b&gt; so it stays text. Some XML libraries skip that step in one special case. HTML treats the contents of elements like <script> as raw text, and a library that applies an HTML rule to an XML document writes the text out unescaped. When the result is read again, what used to be text has become elements:

original:      <script>&lt;b&gt;hi&lt;/b&gt;</script>   → script contains the text "<b>hi</b>"
written back:  <script><b>hi</b></script>               → script contains a new <b> element

Different parsers can read the same text and interpret unusual or malformed input differently. Two parsers can build different trees from unusual input. So can one parser reading the original text and then reading a written-back copy. Malformed DOCTYPE declarations are a classic example, and SAML never uses them. If two components each parse their own copy of a response, they can end up looking at different documents.

Why this matters

Taken together, these points show how one SAML response can mean one thing to the signature verifier and another to the application:

  • The same tree can be written many ways, so the signature depends on canonicalization reproducing exactly the bytes that were signed.
  • The signature covers bytes, but the application reads a tree, so the two have to describe the same content.
  • The signature reaches the assertion only indirectly: through a manifest, an ID, and a hash that leaves out the signature itself.
  • The message itself chooses how it's canonicalized, and anyone submitting it can change that choice.
  • Each time a program converts between text and a tree, or uses a second parser, it creates another copy that can drift from the original.

Each of these is a point where the two views can diverge. A service provider stays safe only if every decision about the user is made from exactly the content the signature verified.

The Vulnerabilities

A SAML response reaches WorkOS through a browser, so the submitted XML must be treated as untrusted input. The signature verifier establishes which content is authentic. Our application then uses that content to validate the sign-in and determine the user's identity.

Our implementation did not consistently bind those steps together. It parsed responses more than once and used different XML parsers. It also converted a parsed response back to text before passing it to the signature verifier. Crafted XML could make the verifier and the application read different content from the same XML document. This “dual-view” gap is the shared root of the four vulnerabilities below:

  1. Trusting unverified reference information. xml-crypto behavior around reference selection and canonicalization, combined with our use of unverified reference information, let the application take identity from an element the verified signature did not cover.
  2. Malformed XML handling. Malformed DOCTYPE handling and related constructions could change which signature was selected for verification.
  3. Changing XML before signature verification. Serializing a parsed response could turn text into XML elements before verification.
  4. Identity outside the signature check. Identity extraction could read content excluded from the verified signature coverage.

In each case, a genuine signature could pass verification while our application read attacker-controlled identity fields. WorkOS was responsible for ensuring that the identity used for sign-in came from verified content. Our integration failed to enforce that requirement consistently.

Our Response

We released an initial patch that rejected the relevant payloads less than two hours after the first report. Our continued testing and collaboration with Alex Tan and our customers identified further variations across these vulnerabilities.

During the incident response process, we shipped immediate mitigations that blocked the identified constructions and variants of the four vulnerabilities:

  • Reject the XML constructions used in the attacks, which legitimate identity providers do not send
  • Verify the signature against the original received XML bytes
  • Compare verified content with the content that WorkOS reads and reject the response if there is a mismatch

On October 1, 2026, we deployed structural hardening on top of those controls: sign-in validation and identity extraction now use the content returned by the signature verifier through xml-crypto's getSignedReferences() API, including for encrypted assertions. The change makes verified content the input to the subsequent sign-in checks.

We tested the reported attack payloads as we introduced protections and checked compatibility against legitimate identity-provider responses. We also deployed additional request-level logging and detections for suspicious XML in accepted requests and patterns of rejected attempts.

Our review of the relevant system logs identified no evidence of exploitation beyond security testing by the external researcher and the customers who received the reports.

Guidance for Developers Processing Signed XML

Current xml-crypto documentation deprecates getReferences() and the references property for obtaining signed XML and directs consumers to getSignedReferences(). Developers using these APIs should review how their application selects the content it trusts after verification.

A successful signature check establishes authenticity for the content covered by that signature. The application must preserve that relationship through the rest of the sign-in flow. For teams reviewing a SAML implementation:

  • Use verified content for sign-in decisions. After successful signature verification, read identity fields from the verified content. Check that the expected response or assertion is covered before using it.
  • Review every parse and conversion. Trace the submitted XML through the verifier and the code that extracts identity fields. Test whether different parsers, or conversion back to text, can change which elements each step sees.
  • Validate the SAML response beyond its signature. Verify the issuer and intended audience, and enforce the applicable destination, time, and request-correlation checks. Using a verified-content API is one part of this validation.
  • Test unexpected input through the full sign-in flow. Include malformed XML and content excluded by signature transforms. A test should establish which identity the application would accept, as well as whether signature verification succeeds.

For further reading, see the xml-crypto verification documentation on obtaining signed content and GitHub’s analysis of SAML parser differentials for a similar dual-view failure in ruby-saml.

Conclusion

Our follow-up work includes broader automated security testing of the SAML implementation and its dependencies. We are also working to monitor upstream changes that affect our integration and reduce the time required to deploy urgent security fixes. Further detection work will focus on suspicious SAML payloads and unusual parsing errors.

We thank Alex Tan (ahacker1) and the customer security teams who reported these issues and helped validate the fixes. Read more about our responsible disclosure program for reporting security vulnerabilities.