3 min readSAML · SSO · AUTHENTICATION

SAML RelayState and the trust you inherit from an IdP

RelayState as an open-redirect and phishing vector through a trusted identity-provider domain, plus the assertion-signing pitfalls that turn a redirect into a full SSO bypass.

SAML is old enough that people trust the parts they should be checking. Two of those parts are easy to test and high-value: where RelayState is allowed to point, and whether the service provider really verifies what it claims to verify on the assertion.

RelayState is an open redirect wearing a trusted domain

RelayState is the deep-link the service provider wants the user returned to after authentication. The SP is supposed to keep it opaque, validate it against an allowlist, or store it server-side keyed by a token. Plenty do none of that and echo it straight into a Location header.

http
GET /sso/login?RelayState=https://app.example.com/dashboard HTTP/1.1
Host: idp.example.com

Swap the value and watch where you land:

plaintext
RelayState=https://attacker.com/harvest
RelayState=//attacker.com
RelayState=/\attacker.com
RelayState=https://app.example.com.attacker.com

If the post-authentication bounce follows that to an attacker origin, you have an open redirect, and an unusually dangerous one. The redirect is initiated from idp.example.com, a domain users and their security teams are trained to trust for login. A phishing link that starts at the real IdP, authenticates the user for real, and then lands them on a credential-harvesting clone is far more convincing than a cold phishing page, because every early signal is genuine.

The assertion is the whole game

RelayState is the appetiser. The assertion is where SSO bypass lives, because the assertion is the SP’s only evidence of who you are. Two failures recur.

Unsigned or partially-signed assertions. SAML lets you sign the response, the assertion, both, or neither, and the SP decides what it requires. A surprising number accept a response whose assertion is unsigned, or sign-check the outer response but read identity from an inner unsigned assertion. If the SP does not strictly require a valid signature over the exact assertion it trusts for identity, you craft an assertion naming any NameID you like:

xml
<saml:Assertion>
  <saml:Subject>
    <saml:NameID>admin@example.com</saml:NameID>
  </saml:Subject>
  <!-- no Signature, or a signature the SP never actually verifies -->
</saml:Assertion>

If that logs you in, the SP was trusting the document’s contents rather than its signature. That is a complete authentication bypass.

XML Signature Wrapping, conceptually. XML signatures cover a specific element, identified by reference. The trick, at a high level, is the gap between which element was signed and which element the SP reads for identity. An attacker keeps a legitimately signed assertion intact so signature verification passes, but restructures the document so the SP’s parser pulls the NameID from a second, injected, unsigned assertion instead. The signature is valid, just not over the data that actually decides who you are. The defence is that signature verification and identity extraction must operate on the same element, by the same reference, with no ambiguity about which assertion is authoritative. I am deliberately not giving a copy-paste payload here, the point is the class: verify-here, read-there is the flaw, and you test it by checking whether the SP’s notion of “the signed assertion” can ever differ from “the assertion I read identity from.”

How I approach an SSO integration

  1. Map both legs: IdP-initiated and SP-initiated. They often validate differently.
  2. Hit RelayState everywhere it appears for redirect handling, and weight any finding by the trust of the host doing the bounce.
  3. On the ACS endpoint, test what the SP actually requires: strip the signature, swap the NameID, and see what it accepts. The answer to “what does this SP trust, the signature or the bytes?” is the entire severity.

RelayState gets you a high-trust phishing redirect. The assertion, when its signing is loose, gets you anyone’s account. Test both, and do not stop at the redirect when the assertion is right there.