rest-context.php
Root helpers for the question "what request is this?" — which PATH does it address, and is it served by the REST API?
Both answers used to be reconstructed ad hoc at every call site, and both
were wrong in the same way: they trusted a value that looks authoritative
but is not (?rest_route= for REST-ness, wp_parse_url() for the path).
Audit module 27, findings BK-1 / BK-2 / CM-7: two guards derived REST-ness
from the raw, client-controlled rest_route request parameter — the
interceptor via a bare isset(), the rate limiter via str_contains().
Both were bypassable by simply appending ?rest_route= (BK-1) or
?rest_route=…challenge (BK-2) to a request that WordPress never serves
through the REST API at all. The same value-independent-existence pattern
had already come back once as a regression in the login gate. It therefore
lives here exactly once, and every consumer calls into it.
Table of Contents
Functions
- creationell_captcha_request_path() : string
- Returns the path portion of the current request, the way WordPress core derives it — NOT the way a URL parser would.
- creationell_captcha_request_target() : string
- Der aktuelle Request als relativer Ziel-URI: Pfad aus `creationell_captcha_request_path()`, Query aus derselben Zeichenkette.
- creationell_captcha_current_rest_route() : string
- Returns the REST route the current request actually addresses.
- creationell_captcha_normalize_rest_route() : string
- Normalises a raw `rest_route` value into the plugin's canonical route form.
Functions
creationell_captcha_request_path()
Returns the path portion of the current request, the way WordPress core derives it — NOT the way a URL parser would.
creationell_captcha_request_path() : string
The return value is still percent-encoded (the "wire spelling"); callers that
need the decoded form apply rawurldecode() to it exactly once, after this
function has split off the query.
WHY THIS EXISTS (audit module 27, finding C1)
Every call site used to run wp_parse_url( $_SERVER['REQUEST_URI'], PHP_URL_PATH ).
wp_parse_url() is a URL parser, and REQUEST_URI is not a URL — it is a
request target. For a target that starts with // the parser treats the
string as a scheme-relative URL (it prepends placeholder:, see
wp-includes/http.php), so the first segment becomes the HOST:
REQUEST_URI wp_parse_url(…, PHP_URL_PATH) WP::parse_request()
/kontakt/ '/kontakt/' kontakt
//kontakt/ '/' kontakt ← same page
///kontakt/ '' kontakt ← same page
Core reduces the target in WP::parse_request() with
list( $req_uri ) = explode( '?', $_SERVER['REQUEST_URI'] ); $req_uri = trim( $req_uri, '/' );
(wp-includes/class-wp.php) and therefore routes all three spellings to the
same page. The guards saw / or '', matched no pattern, and let the
request through: POST //kontakt was a one-character bypass of the entire
interceptor path guard.
WHAT THIS DOES
- Split off query and fragment BEFORE anything is decoded, so a
percent-encoded
?inside the path cannot smuggle a query string in. - Strip scheme and authority if the target arrived in absolute form
(
POST http://example.com/kontakt HTTP/1.1, RFC 9112 §3.2.2). Core does not do this and simply 404s such a request, butwp_parse_url()did — so keeping it guards MORE than core routes, never less. Dropping it would have taken protection away that 1.0.2 had. - Collapse leading slashes to exactly one — the same reduction core's
trim( $req_uri, '/' )performs. INNER double slashes are kept, because core keeps them too, and the TRAILING slash is kept, because existing patterns are written against the spellingwp_parse_url()produced.
Tags
Return values
string —Request path with exactly one leading slash, still encoded.
creationell_captcha_request_target()
Der aktuelle Request als relativer Ziel-URI: Pfad aus `creationell_captcha_request_path()`, Query aus derselben Zeichenkette.
creationell_captcha_request_target() : string
WARUM DAS EINE EIGENE FUNKTION IST (Nachlese Modul 27, Strang N1; Umfang erweitert im Re-Review und in der Nachlese N6)
remove_query_arg() OHNE URL-Argument und add_query_arg() ohne URL-
Argument sind dieselbe Mechanik: beide delegieren an add_query_arg(), und
dessen Zweig count( $args ) < 3 (bzw. < 2 bei der Array-Form) liest
$_SERVER['REQUEST_URI'] ROH. Sie leiten also eine Pfadangabe aus dem
Request ab, ohne das sichtbar zu tun — deshalb standen die betroffenen
Stellen in keinem der vier Review-Berichte.
Gemessen wurde das am Ausblenden-Link des Härtungs-Hinweises: bei
REQUEST_URI = "//wp-admin/admin.php?page=…" rendert er ein
protokollrelatives href="//wp-admin/admin.php?…"; esc_url() reicht jede
mit / beginnende Zeichenkette unverändert durch. Der Browser liest das als
Host wp-admin, der Klick kommt nie an. Dieselbe Konstruktion stand am
„↻ Aktualisieren"-Link der Statistik-Seite.
Die Query kommt bewusst aus DERSELBEN Zeichenkette wie der Pfad, damit
beide Hälften zusammenpassen und nicht die eine aus REQUEST_URI und die
andere aus QUERY_STRING stammt (die beiden können auseinanderlaufen, etwa
hinter einer Rewrite-Regel). Und der Rückgabewert ist bewusst RELATIV: er
ersetzt genau das, was der Kern ohne URL-Argument selbst eingesetzt hätte.
VOLLZÄHLIGKEIT: Seit der Nachlese N6 übergibt jeder
add_query_arg()/remove_query_arg()-Aufruf im ausgelieferten Code eine
explizite URL — entweder diese Funktion, eine $base_url aus
menu_page_url() oder admin_url(). Nachgezählt wird das nicht von Hand,
sondern von tests/test-hardening-migration.php (Abschnitt „Bestandsaufnahme"),
das alle Aufrufe unter includes/ auszählt und rot wird, sobald einer wieder
ohne URL-Argument dasteht. Ein früherer Stand dieses Docblocks nannte die
Redirect-Funktion der Härtungs-Migration „die neunte und letzte Stelle im
Plugin, die eine Pfadangabe aus dem Request ableitet" — das war messbar
falsch und ist hiermit ersetzt.
Tags
Return values
string —Relativer Ziel-URI (führender Schrägstrich, genau einer).
creationell_captcha_current_rest_route()
Returns the REST route the current request actually addresses.
creationell_captcha_current_rest_route() : string
Returns a normalised route (leading slash, no trailing slash, no query
part) — for example /creationell-captcha/v1/challenge — or the empty
string when the request is not served by the REST API.
The function is deliberately conservative: whenever it cannot establish that WordPress core will hand the request to the REST server, it returns the empty string. Both consumers fail closed on that answer (the interceptor guards the request, the rate limiter counts it), so under-detection is safe while over-detection hands out a bypass.
It must work at init priority 0/1, i.e. long before parse_request has
populated $GLOBALS['wp']->query_vars and before core defines the
REST_REQUEST constant, so it reconstructs core's own decision from the
request instead of reading either of those.
Tags
Return values
string —Normalised REST route, or '' when this is not a REST request.
creationell_captcha_normalize_rest_route()
Normalises a raw `rest_route` value into the plugin's canonical route form.
creationell_captcha_normalize_rest_route(string $raw) : string
Returns '' for every value that does not address a concrete REST route.
Parameters
- $raw : string
-
Raw
rest_routevalue as core would see it.
Tags
Return values
string —Route with a leading slash and without a trailing one, or ''.