UnderAttack
in package
Gates anonymous front-end page views behind an interstitial proof-of-work challenge while under-attack mode is active. See the module-5 design spec.
Table of Contents
Constants
- COOKIE = 'creationell_captcha_ua_pass'
- The pass cookie name.
- FIELD = 'creationell_captcha_ua'
- The interstitial form field carrying the solved challenge.
Methods
- run() : void
- Runs the under-attack gate. Registered on `template_redirect`. Terminates the request when an interstitial is served or a pass redirect is issued.
- argon2id_worker_snippet() : string
- Returns the inline Argon2id worker-registration script, or '' when the Argon2id algorithm is not in use.
- commit_pass() : bool
- Sets a minted pass token as the visitor's cookie.
- has_valid_pass() : bool
- Whether the request carries a valid, unexpired pass token that belongs to THIS visitor.
- mint_pass() : array{token: string, expiry: int}|null
- Mints a fresh pass token WITHOUT setting anything.
- redeem() : bool
- Redeems a posted interstitial solution: mints the pass FIRST, spends the challenge SECOND, sets the cookie LAST.
- request_target() : string
- The URL the interstitial posts back to, and the one the visitor returns to after passing the gate.
- serve_interstitial() : void
- Outputs the interstitial page with HTTP 503 and terminates.
- verify_gate_payload() : bool
- Verifies the payload the interstitial posted back — as the under-attack gate, which is the only caller allowed to redeem a challenge that was issued under ctx suppression (FU-1).
Constants
COOKIE
The pass cookie name.
private
mixed
COOKIE
= 'creationell_captcha_ua_pass'
FIELD
The interstitial form field carrying the solved challenge.
private
mixed
FIELD
= 'creationell_captcha_ua'
Methods
run()
Runs the under-attack gate. Registered on `template_redirect`. Terminates the request when an interstitial is served or a pass redirect is issued.
public
run() : void
argon2id_worker_snippet()
Returns the inline Argon2id worker-registration script, or '' when the Argon2id algorithm is not in use.
private
argon2id_worker_snippet() : string
Return values
stringcommit_pass()
Sets a minted pass token as the visitor's cookie.
private
commit_pass(array{token: string, expiry: int} $pass) : bool
Parameters
- $pass : array{token: string, expiry: int}
-
Value from
mint_pass().
Return values
bool —True when the cookie was handed to PHP.
has_valid_pass()
Whether the request carries a valid, unexpired pass token that belongs to THIS visitor.
private
has_valid_pass() : bool
BK-3: the check used to be hash_hmac('sha256', $expiry, $secret) — the
MAC covered the expiry timestamp and nothing else. One solved
interstitial therefore produced a transferable bearer token: copy the
cookie value, hand it to any number of clients, and every one of them
walked past the gate until underattack_pass_duration (up to 86400 s)
ran out. The verification now happens against a token whose MAC also
covers a fingerprint of the visitor that the cookie does not carry —
see creationell_captcha_underattack_pass_binding() for the trade-off
and the filter that switches it off.
Return values
boolmint_pass()
Mints a fresh pass token WITHOUT setting anything.
private
mint_pass() : array{token: string, expiry: int}|null
Both failure modes are permanent-until-fixed as often as they are
transient, and both leave the site behind a 503 that nobody can pass
(m4). has_valid_pass() fails closed for the same missing key, so the
gate is shut in both directions — which is the right direction, but the
operator used to get no hint at all: no log line, no doctor check, just a
site that stays down while visitors solve proof-of-work after
proof-of-work. Hence the two log lines below.
Return values
array{token: string, expiry: int}|null —Null when no pass could be minted.
redeem()
Redeems a posted interstitial solution: mints the pass FIRST, spends the challenge SECOND, sets the cookie LAST.
private
redeem(string $solution) : bool
The order is the finding (m5). Before 1.1.0 the gate verified first and
issued afterwards — and verification claims the challenge's single-use
replay marker (Engine::verify()), so every failure of the issuing step
burned a proof-of-work the visitor had just completed and sent him back
to a fresh interstitial. Where the cause is per-request that costs one
extra PoW; where it is permanent — output already sent before
template_redirect, or no derivable HMAC key (m4) — it is an endless
loop of them, and nothing in the log said why.
Minting has no side effect, so it can safely go first. Only the cookie is set after the challenge is spent, and by then the two reasons the issuing step can fail have already been ruled out.
Parameters
- $solution : string
-
The raw payload from the interstitial form field.
Return values
bool —True when the visitor passed AND the pass cookie was set.
request_target()
The URL the interstitial posts back to, and the one the visitor returns to after passing the gate.
private
request_target() : string
C1, sixth call site: both used to be the raw REQUEST_URI. Two distinct
consequences, neither of them cosmetic:
- As the form
actionthe value is escaped withesc_url(), andesc_url()returns every string starting with/unchanged (wp-includes/formatting.php:if ( '/' === $url[0] ) { $good_protocol_url = $url; }). A request for//fremder.host/x— which WordPress happily serves, it just 404s — therefore producedaction="//fremder.host/x", a protocol-relative URL to a foreign host. The page auto-submits, so the browser POSTs the solved challenge there without the visitor doing anything. - As the redirect target
wp_validate_redirect()does catch the foreign host, but it catches it by falling back to wp-admin: a LEGITIMATE visitor who requested//kontakt/passed the gate and then landed in the dashboard instead of on his page.
creationell_captcha_request_path() collapses the leading slashes to
exactly one, which removes both. The query string is kept — dropping it
would send the visitor to a different page than the one he asked for
(/shop/?s=stuhl). The fragment is dropped because browsers never send
one.
Return values
stringserve_interstitial()
Outputs the interstitial page with HTTP 503 and terminates.
private
serve_interstitial() : void
verify_gate_payload()
Verifies the payload the interstitial posted back — as the under-attack gate, which is the only caller allowed to redeem a challenge that was issued under ctx suppression (FU-1).
private
verify_gate_payload(string $payload) : bool
Deliberately NOT creationell_captcha_verify_payload() (includes/widget.php):
that helper is the FORM path, and the whole point of the marker is that
the form path keeps rejecting these challenges. The distinction is made
by this call site — it is a PHP argument, not a request field, so no
anonymous sender can claim it for himself.
Everything else stays identical to the form helper: the kill switch is already handled in run() above, and the base64 pre-check below is the same one, kept so a junk field never reaches the decoder.
Parameters
- $payload : string
-
The raw payload from the interstitial form field.