CreaCaptcha

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

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
string

commit_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
bool

mint_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 action the value is escaped with esc_url(), and esc_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 produced action="//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
string

serve_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.

Return values
bool

        
On this page

Search results