SSO Attack Vectors
The places SSO implementations get broken. Most are protocol-level mistakes that look harmless until exploited. Knowing the attacks helps you implement validators that actually validate.
Golden SAML
If an attacker compromises the IdP’s signing key, they can forge SAML assertions for any user against any SP that trusts that IdP.
Real incident: SolarWinds (2020) — attackers extracted the SAML signing certificate from compromised Active Directory Federation Services servers, then forged assertions to access multiple downstream services (Microsoft 365, etc.).
Defense (mostly at the IdP):
- Hardware-backed signing keys (HSM, TPM).
- Monitor for cert exports / unusual signing activity.
- Short cert lifetimes + rotation.
- Multi-source attestation for high-value access (don’t trust SAML alone).
App-side defense: short access tokens, anomaly detection on login patterns, additional authentication for sensitive operations.
Signature Wrapping (XSW)
XML Signature Wrapping exploits the gap between what the signature validator sees and what the assertion parser sees. Possible because XMLDSig validates the canonicalized XML against a Reference ID — but XML structure can be manipulated to keep the signed element while moving the “active” data elsewhere.
<Response>
<Assertion ID="signed-id">
<Subject>alice</Subject> <!-- signed; parser ignores -->
</Assertion>
<Assertion> <!-- not signed; parser reads -->
<Subject>admin</Subject>
</Assertion>
<Signature>... signs ID=signed-id ...</Signature>
</Response>
The signature validates (the signed-id element is intact). The application parser may pick up the OTHER assertion. Auth as admin.
This isn’t theoretical. SAML libraries have had XSW bugs repeatedly. Defenses:
- Use a battle-tested library, kept current.
- Validate that the signed element IS the element you process —
signed_id == used_id. - Reject responses with multiple Assertion elements.
- Schema-validate strictly.
python3-saml (and the upstream OneLogin libraries) have had XSW CVEs; check yours is patched.
SAML response without signature validation
Some libraries by default validate only the assertion, or only check signature presence without verifying it matches the configured IdP cert. Result: any signed-by-anyone assertion is accepted.
Worst case: app accepts an unsigned assertion if signing is “optional.” This is a CVE-worthy bug.
Always require signed responses + verify against the specific IdP’s cert from metadata.
Redirect URI manipulation
OAuth/OIDC’s redirect_uri must match a registered URL. Common mistakes:
- Wildcard matching (
https://*.example.com/callback) lets an attacker register a subdomain (or take over an abandoned one) and steal codes. - Path matching but allowing query strings —
https://app.example.com/callback?next=https://evil.com. - Open redirect at the registered URI:
https://app.example.com/callbackreturns 302 to?next=— attacker chains them.
Defenses:
- Exact match on
redirect_uriregistration. - No wildcards in production.
- No open redirects in your app — validate or whitelist any
next=param.
Authorization code interception
In native apps and SPAs without PKCE, the auth code returned to the redirect_uri can be intercepted:
- A malicious app registering the same URL scheme (mobile).
- A network-level attacker sniffing.
- A logged-in attacker on the same machine.
PKCE makes the code worthless without the original verifier. Always use PKCE.
Token leakage via referer or history
URL-fragment tokens (implicit flow) end up in:
- Browser history.
- HTTP Referer headers to subsequent requests.
- Server access logs (if the page makes any request that includes the URL).
This is why implicit flow is deprecated. Even with code flow, beware of putting tokens in URL query parameters anywhere.
State / nonce missing or unverified
@app.get("/callback")
def callback(code: str, state: str):
tokens = oauth.exchange(code)
return create_session(tokens)
No verification of state against the one stored at authorize. Result: CSRF — attacker initiates a flow, lures victim into completing it, victim’s session ends up logged in as attacker’s account.
Defense: store state in a server-side cookie when sending the authorize request; verify it matches on callback. Same for nonce in the ID token.
Token replay
An intercepted token (SAML assertion, ID token, access token) is re-presented later.
Defenses:
- Short token lifetimes (minutes).
- Validate
exp,nbf, SAMLNotOnOrAfter. - Track one-time use IDs (
jtifor JWT,IDfor SAML) for the duration of token validity. - For ID token specifically: validate
noncematches the value you sent.
IdP-initiated SSO replay
IdP-initiated SAML (no InResponseTo) is particularly vulnerable:
- Attacker captures a victim’s old assertion.
- Replays it later to the SP.
- SP creates a session as the victim.
Defenses:
- Track recently-seen assertion IDs and reject duplicates.
- Strict
NotOnOrAfter(short window). - Prefer SP-initiated flows.
Some SPs disable IdP-initiated SSO entirely.
alg: none JWT vulnerability
A historic class of bugs in JWT libraries: accepting the alg: none header (claiming no signature) as valid. Forged token with no signature → library accepts it.
Defense:
- Use libraries that reject
alg: noneby default. - Configure the verification to accept only specific algorithms (
["RS256"]), not “whatever the token says.”
alg confusion (HS256/RS256)
The classic JWT bug:
- Server expects RS256 (asymmetric, validates with public key).
- Attacker sends a token with
alg: HS256(symmetric). - Library reads the public key file as the HMAC secret and validates.
The public key is, well, public — attacker can forge a valid HS256-signed token with it.
Defense: hardcode the expected alg in verification, never let the token tell you.
Token leakage through logs
Tokens (especially JWTs with PII) end up in logs:
- Request access logs (if token in URL or full headers logged).
- Application logs (debug logging the request).
- Error reports with full request capture.
Defenses:
- Never put tokens in URL query parameters.
- Scrub Authorization headers in log middleware.
- Configure error reporters (Sentry) to scrub sensitive headers.
Subdomain takeover
If your redirect_uri is at auth.example.com and you stop using it, an attacker can claim the subdomain (DNS pointing somewhere abandoned) and capture future auth codes.
Defense: monitor your DNS, decommission subdomains carefully, don’t register them at OAuth clients if you might abandon them.
CSRF on logout
If /logout is GET-able, an attacker triggers it via image tag:
<img src="https://app.example.com/logout">
The user is silently logged out. Annoying, not catastrophic — but the same hole means an attacker can disrupt user workflows.
Defense: logout is POST with CSRF protection, or SameSite cookies (most browsers default protect now).
Phishing the OIDC consent screen
Attacker registers a malicious OAuth app on your IdP (if the IdP allows arbitrary registration) and tricks the user into consenting:
“Sketch wants permission to read your email, calendar, and contacts.”
Many users click through.
Defenses (IdP side):
- Require admin approval for new app registrations in enterprise tenants.
- Prominent consent screen (“you’re granting access to YOUR data”).
- Warn for newly-registered or unverified apps.
Defenses (your side as a user): don’t consent to apps you don’t recognize.
DNS rebinding against the IdP
An attacker’s site uses DNS rebinding to make the browser send authenticated requests to localhost or internal IdPs. Defenses are mostly at the IdP (host header validation, CORS).
For your app: validate Host header, set strict CORS, restrict the OIDC token endpoint to backend-only access if possible.
Account takeover via email change
A subtle one. Your app maps users by email (instead of sub). Attacker creates a new IdP account with the victim’s email (perhaps the IdP doesn’t verify email). On SSO login, your app matches the email and grants the attacker access to the victim’s account.
Defense: match on sub. If sub is missing for some reason, require email verification at the IdP (email_verified: true in the claims).
Mixed-up clients
OAuth attacks where the user starts an auth flow with IdP A but the attacker intercepts and routes to IdP B. The app receives codes/tokens from B but processes them as if from A.
Mitigation: validate iss in the ID token matches the OP you initiated with. Bind state to the specific OP.
Common pitfalls in implementations
- Library defaults that are too permissive (Symfony Security, older python-saml versions).
- Hand-rolled SAML parsing — bound to be wrong somewhere.
- Trusting
audarray without checking your client_id is in it. - Not validating
exp, treating “old token” as valid. - Reusing libraries with known CVEs without updating.
- Accepting plain HTTP redirect URIs (
http://rather thanhttps://) in production.
Defense checklist
For OIDC RPs:
- Authorization code flow with PKCE.
- Validate
iss,aud,exp,nonceon ID token. - Verify signature against JWKS (with
kidmatching). - Hardcode acceptable
alg(e.g.["RS256"]). - Exact-match
redirect_uriregistration; HTTPS only. - CSRF via
stateparameter. - Short access token lifetime, refresh token rotation.
For SAML SPs:
- Validate signature against IdP’s cert from metadata.
- Validate
Issuer,Destination,Audience,Conditions/NotOnOrAfter. - Reject responses with multiple Assertions (XSW prevention).
- Track
IDfor replay prevention. - Strict schema validation.
- Use a library with current CVEs patched.
Common interview confusions
- “HTTPS prevents token leakage.” — HTTPS protects in transit, not from XSS, logs, browser history, or compromised endpoints. Storage and lifetime matter too.
- “Signature verification is enough.” — must also verify the claims (aud, iss, exp, nonce, audience). A valid signature on a token for someone else is still wrong.
- “PKCE is only for mobile apps.” — recommended for all flows including confidential clients. OAuth 2.1 makes it mandatory.
Interview angle
- “What’s Golden SAML?” — attacker steals the IdP’s signing key (typically by compromising AD FS), forges SAML assertions for any user. Defense: HSM-backed signing keys at the IdP, short-lived assertions, anomaly monitoring.
- “What’s an XML Signature Wrapping attack?” — exploits the gap between what the signature validator sees and what the assertion parser processes. Crafted XML keeps the signed element while putting the “active” data elsewhere. Mitigate with battle-tested libraries and same-element validation.
- “Why is PKCE important?” — protects against authorization code interception. Without it, an attacker who steals the code (intercepted from redirect URI) can redeem it. With it, they also need the
code_verifieronly the original client has. - “What’s the
alg: noneJWT attack?” — old library bug that accepted unsigned tokens. Defense: configure verification with explicit algorithm allowlist (["RS256"]), not “whatever the token’s alg field says.” - “How do you protect against authorization code CSRF?” —
stateparameter: random value sent in the authorize request, stored server-side, verified on callback. Mismatch → reject. - “Why match users on
sub, not email?” — emails change (user changed name, departed); attackers might create accounts with the victim’s email.subis stable and IdP-issued. - “What’s
redirect_urivalidation and why is it critical?” — exact-match registered URI prevents attackers from redirecting auth codes elsewhere. No wildcards, no open redirects, no path manipulation.