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
- Capture a valid SAML response with a proxy and post it a second time. Expect rejection.
- Post it after the
NotOnOrAftertime. Expect rejection. - Send it to a second server instance to confirm the shared store works.
- Submit a response whose
InResponseTodoes 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.