A SAML service provider redirects to whatever RelayState comes back with the response — what is the defect?
answer
- state that came back is input
- the responder echoes it unchanged
- outside the message's enveloped signature
- your own callback becomes a redirector
- carry a key, not a destination
basics
~20 sRelayState is opaque data echoed back through the browser, not a trusted destination. A service provider that redirects to it unvalidated turns its own assertion-consumer endpoint into an open redirect to any address an attacker supplies.
solid answer
~50 s`RelayState` exists to carry the service provider's own state across the detour: it is set when the login starts, the responder must return exactly the value it received, and it is opaque to the identity provider. The defect is treating that returned value as a destination. It travels beside the message rather than inside it, so the enveloped `<ds:Signature>` within the `<samlp:Response>` does not cover it — anything that reaches the browser can change it, and in an unsolicited login the value was never the service provider's to begin with. Redirecting to it unvalidated makes the service provider's own callback a general-purpose redirector on its trusted origin, at the moment the reader has just logged in. The fix is to stop carrying a URL: make `RelayState` an opaque key into state stored with the pending request, and fall back to a fixed landing page.
code
http · 5 linesPOST /acs HTTP/1.1
Host: loans.example
Content-Type: application/x-www-form-urlencoded
SAMLResponse=PHNhbWxwOlJlc3BvbnNlIC4uLjwvc2FtbHA6UmVzcG9uc2U%2B&RelayState=https%3A%2F%2Fattacker.example%2Fharvestgo deeper
Remember that RelayState is state the service provider sent out and got back through the browser, which makes it input on return rather than something the service provider still knows.
Explain that the responder echoes it unmodified, that it sits outside the message's enveloped signature, and why a redirect straight to it is an open redirect on a trusted origin.
Give the design: opaque key, target stored with the pending request, default landing page on any miss, and an allow-list of relative paths where a deep link is genuinely required.
Set the rule once for every federated integration you own, since each new counterparty is another callback that will otherwise reinvent a redirector on your own domain.
## What RelayState is for A reader following a citation into the inter-library loan service is trying to reach one page: the request form for volume 4718. The login detour goes to another organisation entirely and comes back through the browser. `RelayState` is the mechanism SAML 2.0 provides for carrying that "where were we" across the round trip. The service provider sets it when it sends the reader away; the responder returns **exactly** the data it received, unmodified, alongside the response. To the identity provider the value is opaque — it is not supposed to interpret it, only to give it back. In an unsolicited IdP-initiated login the same slot is used in the opposite direction: the identity provider sets it, usually to indicate what the reader clicked, because there is no pending request at the service provider to hold that information. ## Why the returned value cannot be trusted - It travels **beside** the message, as its own field of the transmission, not inside the XML. The enveloped `<ds:Signature>` over the `<samlp:Response>` or the assertion covers the message, not a field sitting next to it. - Some transmissions do protect it separately — a detached signature computed over the whole query string covers `RelayState` along with everything else — but that binds the value to *that transmission by that sender*. It still says nothing about whether the value names a safe destination, and in the unsolicited case the sender is not the party whose users you are protecting. - The whole round trip runs through the browser, which is the one participant neither party controls. So the returned value is untrusted input that happens to have made a round trip. It has no more standing than a query parameter a stranger typed. ## The failure If the service provider ends the login with "redirect the browser to whatever came back in `RelayState`", it has published an open redirect on its own origin, at its assertion-consumer endpoint, and it fires immediately after a successful login. That is the worst possible moment: the reader has just authenticated, expects to be moved, and any warning they might have heeded has already been dismissed. The destination is attacker-chosen and the referring origin is the service they trust. The same carelessness has a second, quieter form. If `RelayState` holds a URL that the service provider will later re-enter as though it were internal state, anything encoded in it — an identifier, a flag, a next step — is now caller-controlled state that the service provider believes it set itself. ## Designing it out 1. **Do not put a URL in it.** Make `RelayState` an opaque, unguessable key. Store the real target alongside the pending request when the login starts, and look it up on return. 2. **Resolve, do not follow.** On return, the value is a lookup key. A hit yields a target the service provider itself recorded; a miss yields the default landing page, never the raw value. 3. **If you must carry a path, constrain it.** Accept only a relative path on the service provider's own origin, reject anything with a scheme, an authority or a protocol-relative prefix, and check it against an allow-list of known entry points rather than a blocklist of dangerous shapes. 4. **Have a default.** Absent, unrecognised or rejected — all three land on one fixed page. An unsolicited login has no recorded target at all, so this path is normal, not exceptional. 5. **Keep it small and keep it boring.** It is a resumption hint, not a transport for application state. ## What RelayState is not - **It is not a request-forgery token.** It carries no binding to the browser that started the login and no integrity of its own; a sender who wants those properties has to build them, and the correlation between an answer and a request lives in `InResponseTo`, not here. - **It is not authorization.** Resolving to a target says the reader was heading there, not that they may have it. The page behind it enforces its own access rules. - **It is not private.** It passes through the browser and through the identity provider, so it must never carry anything sensitive about the reader or the resource. ## The interview version Asked to review an integration, the giveaway line is a redirect whose argument came straight off the response. A strong candidate names the property in one sentence — the value came back through the browser, outside the message signature, so it is input, not state — and then gives the design that removes the question entirely: carry a key, keep the target server-side, default when it does not resolve.
- In an unsolicited IdP-initiated login there is no pending request to key against — what then?Then the service provider has no recorded target and should use a fixed landing page. If a counterparty insists on deep links from its portal, accept only a relative path on your own origin, matched against an allow-list of entry points, and never a value carrying a scheme or authority.
- Does signing the response protect RelayState?Not the enveloped signature inside the XML — it covers the message, and `RelayState` is not in the message. A transmission that signs the whole query string does cover it, but that binds the value to that sender's transmission; it still does not make the destination safe, so the service provider validates it either way.
- What is the practical test for an allow-list of targets?Reject anything with a scheme or authority component, including protocol-relative forms; require a leading single slash; then match the remaining path against known entry points rather than filtering characters. Blocklists on this shape have a long history of being one encoding away from bypass.
saying these in an interview costs you the question
- Calls RelayState a CSRF token that binds the login
- Says the assertion's signature covers RelayState
- Redirects to the returned value after checking it is not empty
- Expects the identity provider to validate the value
- Blocks dangerous characters instead of allow-listing targets
- Packs application state into it and re-trusts it on return