Skip to content

v1.17.0 — Web Bot Auth protocol-00, bounded siteKey state

Choose a tag to compare

@cport1 cport1 released this 27 Jul 16:45
· 106 commits to main since this release

Two trust-boundary fixes. Both are behaviour fixes on top of v1.16.0 — no
configuration change is required to benefit from either.

Web Bot Auth: protocol-00 Signature-Agent

draft-meunier-webbotauth-httpsig-protocol-00 (2026-06-26) changed
Signature-Agent from a bare Structured Fields String to a Dictionary carrying
an optional type parameter that selects how keys are discovered:

sig1="https://a.test"                          directory (default)
sig1="https://a.test/jwks.json";type=jwks_uri  the URL is a JWK Set
sig1="https://a.test/card";type=cimd           Client ID Metadata Document

The Node parser anchored on a closing quote at end-of-string, so any trailing
parameter fell through and failed to parse as a URL. Because verification fails
open on non-cryptographic errors this never produced a false positive — it
silently degraded signers using the two new discovery types from
cryptographically verified to presence-only
, losing exactly the capability
v1.15.0 introduced.

Node now accepts all three forms plus the pre-00 bare string, and resolves
jwks_uri and cimd (including a nested jwks_uri, re-validated and
re-fetched through the same SSRF guard). The Go server needed no parser change —
WebDecoy/web-bot-auth v0.2.0 already handled the dictionary form.

New: 24-hour signature lifetime ceiling. Neither library enforced one. They
reject an already-expired signature, but nothing capped how long a live
signature stays replayable. Signatures whose expires sits more than 24h past
created now drop to presence-only rather than earning the verified verdict —
not scored as forgery, since the draft says RECOMMENDED and a generous TTL is
not evidence of spoofing.

Known divergence: Node resolves cimd; the Go module reports it
unresolvable and fails open. No observed signer uses cimd today —
chatgpt.com and agent.bot.goog both serve directory.

Bounded state for the unvalidated siteKey

siteKey is supplied by the client and validated against no registry, yet it is
the first component of every partition key — pow:{siteKey}:{ip},
{siteKey}:{ip}, {siteKey}:{fp}. A caller varying it per request allocated
unbounded server-side state.

This is not a detection bypass: obtaining a challenge for a site requires
asking with that site's key, which lands in its bucket like any other visitor.
It is a memory-exhaustion vector, and — combined with the previous
clear-everything cleanup — a way to flush legitimate rate-limit and fingerprint
state.

  • SiteKeyGuard caps distinct site keys per source IP (default 8) and folds
    the excess into a single shared bucket. Folding rather than rejecting means a
    caller rotating keys to evade rate limiting ends up sharing one bucket, so
    their requests accumulate against it faster than with a single honest key.
  • LRU-bounded stores replace the unbounded maps and the
    if size > N: clear() cleanup on challenges, used PoW solutions and spent
    tokens. Clearing an entire replay guard once a threshold is crossed was itself
    reachable by an attacker: force the threshold, then replay.

Measured across all three servers — 24 requests from one IP with a rotating site
key previously stayed pinned at PoW difficulty 4 and now escalates to 5,
matching the fixed-key behaviour.

New configuration (both optional)

Variable Default Description
FCAPTCHA_SITE_KEYS (any) Comma-separated allowlist of accepted site keys. Unlisted keys are folded into the shared overflow bucket instead of allocating their own state.
FCAPTCHA_MAX_SITE_KEYS_PER_IP 8 Distinct site keys one IP may allocate state for. The cap itself is unconditional.

Zero-config self-hosting is unchanged: the allowlist is opt-in, and the
per-IP cap applies whether or not it is set.

Tests

New unit suites for the site-key guard and bounded stores in all three
languages, plus regression tests covering protocol-00 typed discovery and the
signature-lifetime ceiling. test/test-detection.js remains at 86/86.