oauth-token-forgery

Forged and unsigned tokens are refused

shipped

The sections below are generated from the CLI (methodology 0.3-draft, CLI 0.3.0); the same text is printed by sealmark explain oauth-token-forgery.

pack
oauth opt-in pack
weight
criticalcandidate
scope
server as deployed (the bearer tokens the endpoint accepts, and what it tells a client that has none)
decided by
httpauthserver
since
0.3-draft (methodology 0.3-draft)
config keys
checks.oauth-token-forgery.enabledchecks.oauth-token-forgery.severityoauth.audienceoauth.issuerserver.headersserver.url

Probes

  • control a well-formed RS256 token from the harness's issuer, unexpired, with the right iss, aud and scope: initialize and tools/list must succeed, or the check is inconclusive
  • alg-none header alg "none", empty signature (header.payload.)
  • alg-none-nodot header alg "none", no signature and no trailing dot (header.payload)
  • alg-None header alg "None", empty signature (header.payload.)
  • alg-None-nodot header alg "None", no signature and no trailing dot (header.payload)
  • alg-NONE header alg "NONE", empty signature (header.payload.)
  • alg-NONE-nodot header alg "NONE", no signature and no trailing dot (header.payload)
  • sig-stripped RS256 header, signature removed (header.payload.)
  • sig-random RS256 header, 256 random bytes in place of the signature
  • payload-tampered a correctly signed token whose payload was changed after signing (sub sm-sub-admin, scope admin)
  • untrusted-key RS256 signed by a key that is not in {jwks}, with the published kid
  • hs256-pem alg HS256 with the published RSA public key (SPKI PEM bytes) as the HMAC secret: algorithm confusion
  • hs256-jwk alg HS256 with the published public key as JWK JSON bytes as the HMAC secret
  • jku RS256 signed by an attacker key, header jku pointing at the attacker key set ({issuer}/attacker/jwks); a fetch of that URL is recorded
  • x5u RS256 signed by an attacker key, header x5u pointing at an attacker URL ({issuer}/attacker/x5u); a fetch of that URL is recorded
  • kid-traversal alg HS256 with an empty key and kid "../../../../../../dev/null"
  • kid-sql alg HS256 keyed with "sm-kid-sql" and kid "x' UNION SELECT 'sm-kid-sql' -- "

Settings and thresholds

Every check also takes checks.oauth-token-forgery.enabled and checks.oauth-token-forgery.severity; they are described once in the configuration reference.

This check has no numeric threshold.

KeyDefaultStandardMeaning
oauth.audiencenonenot lockedThe audience (RFC 8707 resource) the stub's tokens carry and the protected-resource metadata must name; the value of {audience}. null uses server.url in canonical form (no fragment, no trailing slash).
oauth.issuernonenot lockedWhich OAuth issuer the server under test trusts. stub starts the harness's own authorization server on loopback (flag --oauth): {issuer} and {jwks} name it, and the caller identities ({bearer}, {bearer:a}, {bearer:b}) become tokens it signed for {audience}, sent as Authorization: Bearer. Point the server's issuer, JWKS URL and audience at those placeholders in server.env or server.args. external declares a server that trusts its own issuer: only the checks that need no harness-signed token run (oauth-discovery). null declares nothing.
server.headersnonenot lockedHTTP headers sent to the server (url transport). Values under secret-looking names are redacted.
server.urlnonenot lockedURL of a Streamable HTTP server. Alone it must already be running; with command, Sealmark starts it and waits for this URL.

How the verdict is established

  • Scoring. critical and scored: pass = 1 point, part = 0.5, fail = 0. A fail caps the pack's grade at D, whatever the other checks scored.

What to fix

Verify every token's signature with a key from the issuer you configured, and only with an algorithm you allow (for example RS256 only): refuse alg none in any spelling, an empty or wrong signature, a payload that no longer matches its signature, a key not in the issuer's key set, and HS256 tokens on an RSA key (never use the public key as an HMAC secret). Take keys from the configured JWKS URL only, never from a jku, x5u or kid in the token header, and never use kid as a file path or a query. If the check is inconclusive, the server did not trust the harness's issuer: set its issuer to {issuer}, its JWKS URL to {jwks} and its audience to {audience}.

False positives

None known: every probe token is invalid by construction. A probe answered HTTP 500 is not counted as served, but is named in the evidence.

False negatives

Only initialize and tools/list are tried with each token; a server that checks tokens there but not on tools/call is not seen. The x5u URL serves a real certificate chain for the attacker key (a leaf and its own attacker CA, written by the harness's X.509 writer); a server that would trust x5u only for a chain to a CA it already trusts refuses it, which is correct.