Frontend Labs
⋯

Chapter 39 — Federated identity attacks: SAML and advanced OAuth/OIDC

In one sentence

SSO concentrates trust in the IdP and the assertion, so the attacks are on the signature (SAML XML signature wrapping, Golden SAML), on the parsing (XXE, comment injection), and on OAuth/OIDC flow details (redirect_uri, state, iss mix-up, JWKS/kid games, account-linking) — and a single flaw is usually full account takeover.

Where we are

G1 Ch 46 taught OAuth 2.0 authorization-code + PKCE and OIDC basics. This chapter is SAML (which G1 skipped) and the advanced OAuth/OIDC attack classes.

How it works

SAML attacks:

   XML SIGNATURE WRAPPING (XSW)
     The SAML Response contains a signed element (the Assertion, or the
     Response). The attacker adds a SECOND, unsigned, malicious Assertion
     and re-arranges the XML so:
       - the signature-verification code still finds and validates the
         ORIGINAL signed element (signature checks out), BUT
       - the application's business logic (a different XPath / a
         `getElementsByTagName("Assertion")[0]` / a different DOM walk)
         reads the INJECTED assertion -> attacker becomes admin@victim.
     Root cause: "what is verified" != "what is consumed". 8 canonical
     XSW patterns (position of the injected element, referencing tricks).
   GOLDEN SAML (CyberArk, 2017)
     Steal the IdP's TOKEN-SIGNING PRIVATE KEY (e.g. from ADFS:
     `ADFS/PrivateKey`, or the signing cert). Then forge ARBITRARY valid
     assertions offline -- any user, any SP, any groups, any validity --
     with NO interaction with the IdP and NO log there. Persistent until
     the key is rotated. Used post-SolarWinds (UNC2452) for cloud
     persistence.
   SIGNATURE EXCLUSION / STRIPPING
     SP accepts an assertion with NO signature, or `WantAssertionsSigned=
     false`, or only checks the Response signature while consuming an
     unsigned Assertion.
   COMMENT / CANONICALIZATION INJECTION
     NameID = `admin@victim.com<!---->.attacker.com`. If the SP's XML text
     extraction stops at the comment (canonicalization quirk) it reads
     `admin@victim.com` -> takeover. (Duo/Uber/GitHub/others, 2018 --
     "The road to hell is paved with SAML assertions".)
   XXE in the SAML parser (Ch 32).
   REPLAY
     No one-time `Assertion ID` tracking, missing/!checked `NotOnOrAfter`,
     `NotBefore`, `InResponseTo` -> replay a captured assertion.
   AUDIENCE / RECIPIENT / DESTINATION not validated
     An assertion minted for SP-A is accepted by SP-B.
   RelayState -> open redirect / reflected XSS.
   IdP-initiated SSO -- no `InResponseTo` to bind, CSRF-like; prefer
     SP-initiated.
   Library CVEs: `ruby-saml` (multiple critical XSW/DoS, 2024-2025 --
     GitLab/others), `python3-saml`, `xmlsec`, many vendor SDKs.

Advanced OAuth/OIDC:

   redirect_uri
     - must be EXACT-match against a registered value, on the AUTH request
       AND validated again on the TOKEN request. Attacks: an open redirect
       on the client's allowed domain, path/`../`/`%2e` tricks,
       `?`/`#`/`@`/`\` parser confusion, `localhost` or wildcard
       subdomains allowed -> steal the `code`.
   state
     - missing / not validated -> login CSRF, forced login, "code
       injection" (attacker gets a code for THEIR account, delivers the
       callback URL to the victim -> victim's app session is now linked to
       the attacker's IdP identity -> account takeover; or victim is
       logged into the attacker's account -> data harvested).
   OAuth MIX-UP (RFC 9207 fixes it)
     Client supports multiple IdPs. A malicious/compromised IdP tricks the
     client into sending the `code` (or accepting a token) meant for
     IdP-A to IdP-B (the attacker). Mitigation: the `iss` parameter in the
     authorization response, validated; a distinct redirect_uri per IdP.
   ID TOKEN validation
     - not checking signature (`alg:none`; JWKS spoofing via `jku`/`jwk`/
       `kid` header injection; RS256->HS256 key confusion, G1 Ch 46),
       `iss`, `aud`, `exp`, `nonce`. Using an ACCESS token as proof of
       identity (no `aud` for your client, may be opaque).
   kid / jku games
     - `kid` used in a file path / SQL / command -> injection.
     - `jku` pointing at an attacker JWKS if not pinned/allowlisted.
     - `jwk` header (embedded key) trusted.
   PKCE downgrade
     - if the AS doesn't bind "started with PKCE" to "must finish with a
       verifier", an attacker strips PKCE and replays an intercepted code.
   Refresh tokens
     - not rotated, no reuse detection, not sender-constrained (DPoP /
       mTLS) -> a stolen refresh token = indefinite access.
   Scope / consent
     - scope escalation on refresh; "illicit consent grant" / OAuth
       consent phishing -- a malicious app requests broad scopes
       (`Mail.Read`, `offline_access`) and a tricked user consents ->
       persistent mailbox access with no password involved (a major M365/
       Google Workspace attack pattern).
   ACCOUNT LINKING / PRE-ACCOUNT-TAKEOVER
     - the app links a "Sign in with Google" identity to a local account
       by EMAIL without verifying control -> attacker pre-registers the
       victim's email locally; victim later federates in and is merged
       into the attacker's account (or vice versa). Require verified email
       + re-auth before linking.
   Device code flow phishing; dynamic client registration abuse;
   front/back-channel LOGOUT not implemented (session survives IdP
   logout).

Defence (both):

   SAML
     [ ] Use a maintained library, CURRENT version. Track its CVEs.
     [ ] Require signed assertions; validate that the signature COVERS the
         exact element you consume (schema-aware validation, reference
         checking) -- or sign the whole Response and consume only from the
         verified tree. Reject multiple assertions.
     [ ] Validate Audience, Recipient, Destination, NotBefore/NotOnOrAfter
         (with small skew), InResponseTo, and enforce ONE-TIME use of the
         Assertion ID (a replay cache).
     [ ] Disable DTD processing in the SAML XML parser (XXE).
     [ ] Canonicalization-safe NameID/attribute extraction (no
         comment-truncation).
     [ ] Prefer SP-initiated; treat IdP-initiated as higher risk or
         disable.
     [ ] Protect the IdP signing key like a root CA (HSM); monitor IdP
         issuance vs SP consumption to catch Golden SAML; rotate on
         suspicion.
   OAUTH/OIDC
     [ ] OAuth 2.1 / RFC 9700: authorization-code + PKCE for ALL clients,
         no implicit, no ROPC, EXACT redirect_uri matching (auth AND
         token request).
     [ ] Enforce `state` (CSRF) and, in multi-IdP, the `iss` response
         parameter (RFC 9207).
     [ ] Validate every ID-token claim + `nonce`; pin JWKS or allowlist
         `jku`; ignore `jwk`; pin the expected `alg`.
     [ ] Rotate refresh tokens with reuse detection; consider DPoP /
         mTLS-bound tokens for high value.
     [ ] Minimise requested scopes; admin-review third-party OAuth apps in
         the tenant (consent-grant governance); alert on new grants of
         sensitive scopes.
     [ ] Require verified email + step-up auth before account linking.
     [ ] Implement back-channel logout.

Worked example

XML Signature Wrapping to admin, and the reference-checking fix.

   The SP validates the SAML Response like this (pseudocode):
     doc = parseXml(samlResponse)                       // DTDs off, good
     sigEl = doc.xpath("//ds:Signature")[0]
     signedEl = resolveReference(sigEl)                 // -> Assertion#A1
     verify(signedEl, sigEl, idpCert)                   // PASSES
     assertion = doc.getElementsByTagName("Assertion")[0]  // <-- BUG
     user = assertion.xpath("//saml:NameID/text()")
     groups = assertion.xpath("//saml:Attribute[@Name='groups']/...")

   ATTACK: take a legit Response for `bob@securesh.op` (Assertion id=A1,
   signed). Wrap it: keep A1 (with its valid signature) but move it inside
   an unused element (e.g. an `<Extensions>` or a second `<Object>`), and
   ADD a new unsigned Assertion id=EVIL as the FIRST child of Response
   with NameID `admin@securesh.op`, groups `superadmin`.
   - `resolveReference` still finds A1 by its Id -> `verify` passes.
   - `getElementsByTagName("Assertion")[0]` returns EVIL -> the SP logs
     the attacker in as admin.

   FIX:
     - After verification, CONSUME ONLY from the verified subtree:
         assertion = signedEl   // the exact element the signature covered
       (and assert signedEl is an <Assertion> directly under <Response>).
     - Require the signature to reference an Assertion (or the Response
       root), reject if the reference URI is empty, points outside the
       document, or resolves to an unexpected element.
     - Reject Responses containing more than one <Assertion>.
     - Validate Audience/Recipient/Destination/timestamps/InResponseTo and
       one-time Assertion ID.
     - Better: use a library that does schema-aware, reference-checked
       validation (and is current) rather than hand-rolled XPath.

Practice (90 min)

  1. PortSwigger Academy "OAuth 2.0 authentication vulnerabilities" and "JWT attacks" tracks (redirect_uri, state/CSRF, alg:none, JWKS injection, kid traversal/SQLi, key confusion). Do the OAuth account-linking and "authentication bypass via flawed CSRF protection" labs.
  2. SAML: use SAMLRaider (Burp) or samling + a deliberately-vulnerable SP (e.g. an old python3-saml/ruby-saml version in a container). Perform an XSW attack (try several of the 8 patterns); a signature-stripping attack; comment injection on the NameID; XXE in the SAML parser. Then upgrade the library / add reference-checked validation and re-test.
  3. Build the pre-account-takeover via unverified email linking in a toy app; fix with verified-email + re-auth.
  4. Reproduce OAuth mix-up conceptually with two mock IdPs and a client that ignores iss; add iss validation (RFC 9207) and distinct redirect_uris.
  5. Consent-grant: in an Entra/Google Workspace dev tenant, register an app requesting broad scopes, "consent" as a test user, then show the offline access; then apply admin-consent-required + a review workflow.

Common mistakes

  • Verifying one element, consuming another (SAML). The XSW root cause. Consume only from the verified subtree; reject multiple assertions.
  • WantAssertionsSigned=false / accepting unsigned assertions.
  • Not validating Audience/Recipient/Destination/timestamps/InResponseTo, or no one-time Assertion ID. Replay and cross-SP reuse.
  • Hand-rolled SAML XML handling / an outdated SAML library. This library class has a steady stream of critical CVEs. Use a maintained one, keep it current.
  • Non-exact redirect_uri, or validating it only on the auth request.
  • Missing state; treating an access token as identity; not checking nonce/aud/iss; trusting jku/jwk/kid blindly.
  • Account linking by email without verifying control. Pre-account-takeover.
  • Non-rotating refresh tokens; no consent-grant governance. Persistent access from one theft or one tricked click.
  • IdP-initiated SSO enabled by default. Harder to secure; prefer SP-initiated.

Check yourself

  1. Explain XML Signature Wrapping in one sentence — what's the mismatch it exploits?
  2. What is Golden SAML, what does the attacker need, and why is it so persistent?
  3. Name four SAML validations beyond "the signature is valid".
  4. What is the OAuth mix-up attack and what parameter (which RFC) mitigates it?
  5. How does a pre-account-takeover via SSO account-linking work, and the fix?
  6. Why is "consume only from the verified subtree" the correct SAML fix?

Further reading

  • SAML: "On Breaking SAML: Be Whoever You Want to Be" (Somorovsky et al., USENIX 2012 — the XSW paper); "The road to hell is paved with SAML Assertions" (Duo, 2018 — comment injection); CyberArk "Golden SAML" (2017); the ruby-saml 2024-2025 advisories.
  • OAuth/OIDC: RFC 9700 (Security BCP), RFC 9207 (iss parameter), OAuth 2.1 draft, FAPI 2.0; PortSwigger "OAuth 2.0 authentication vulnerabilities" and the "OAuth security" write-ups; "Common OAuth Vulnerabilities" (Doyensec).
  • Tools: SAMLRaider, samling, python-saml/ruby-saml test suites; Burp JWT Editor; oidc-attack.
  • Consent phishing: Microsoft "Illicit consent grant attack" docs; the "app consent governance" guidance.

Generated from real-life-security-guide-v1.md · 8 Oct 2026 16:36