Safeguard
Application Security•

ASVS 5.0 V6.8.3: Process Each SAML Assertion Only Once

SAML assertions must be consumed once within their validity period to stop replay. Track assertion IDs, check conditions, and test it. Level 2.

Safeguard Security Team
2 min read

ASVS 5.0 V6.8.3 requires that SAML assertions are uniquely processed and used only once within their validity period, to prevent replay attacks. It is a Level 2 requirement.

Why it matters

A SAML response travels through the user's browser. It may land in logs, browser history, a proxy, or a screen share. If the service provider accepts the same assertion again, whoever holds a copy can sign in as that user until the assertion expires, with no password and no MFA.

How to implement it

Keep a record of assertion IDs you have accepted, until after they would have expired anyway. The write must be atomic.

def accept_assertion(assertion):
    now = utcnow()
    c = assertion.conditions
    if not (c.not_before - SKEW <= now < c.not_on_or_after + SKEW):
        raise Denied("outside validity")
    if assertion.audience != MY_ENTITY_ID or assertion.issuer != CONFIGURED_IDP:
        raise Denied("wrong audience or issuer")
    # store only if new; expire the record after NotOnOrAfter plus skew
    stored = cache.set_if_absent(
        f"saml:{assertion.id}", 1,
        ttl=(c.not_on_or_after - now).seconds + SKEW_SECONDS)
    if not stored:
        raise Denied("replay")

Also check InResponseTo against the request ID you issued, and keep clock skew small. Use a shared store such as Redis or a database table with a unique constraint, so every node sees the same list.

CREATE TABLE saml_seen (assertion_id text PRIMARY KEY, expires_at timestamptz NOT NULL);

A failed insert on the primary key means replay.

How to test it

  1. Capture a valid SAML response with a proxy and post it a second time. Expect rejection.
  2. Post it after the NotOnOrAfter time. Expect rejection.
  3. Send it to a second server instance to confirm the shared store works.
  4. Submit a response whose InResponseTo does not match a pending request.

Common mistakes

  • Replay cache in process memory.
  • Dropping the record before the assertion expires.
  • Checking time but not the assertion ID.
  • Allowing unsolicited responses with no request correlation when you do not need them.

Quick check

How long should the assertion ID stay in the seen list?

At least until the assertion's validity period plus any clock skew allowance has ended.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.