CreaCaptcha

hardening-migration.php

Fail-safe-Migration: findet gefährliche Bestandskonfiguration und MELDET sie.

Die Eingabeprüfung dieser Version weist vier Werte ab, die frühere Versionen durchgelassen haben: die Präfixlänge 0 in einer IP-Liste (0.0.0.0/0, ::/0 — AF-5/CLI-1), einen bypass_cookies-Eintrag ohne Wert (BK-14) und ein Catch-all-Muster in bypass_ua_allow (BK-9). Dazu kommen zwei Klassen, die keine Eingabeprüfung abweist, weil sie syntaktisch einwandfrei sind: ein IP-Bereich in IPv4-mapped Schreibweise (::ffff:0:0/96), der seit der Kanonisierung aus Befund I2 nichts mehr trifft, und ein UA-Muster wie Stern-Schrägstrich-Stern, das jeden realistischen Kennzeichner trifft, ohne ein blosses Sternchen zu sein (B-M2). Das gilt aber alles nur für NEUE Eingaben. Was bereits in der Option steht, bleibt unangetastet: eine laufende Installation still umzuschreiben, während der Betreiber nichts davon weiß, wäre die schlechtere Variante — der Bypass, den er morgen vermisst, kann der sein, über den sein Monitoring läuft.

Diese Datei ist der Gegenpart dazu. Sie SCHREIBT NICHTS. Sie liest die Einstellungen, benennt jeden Fund mit Liste, Eintrag und Konsequenz und zeigt ihn über beide Kanäle: als Admin-Hinweis im Backend und als Zeile in wp creacaptcha doctor.

Bewusst NICHT gemeldet wird 0.0.0.0/0 in der IP-BLOCKliste: dort heißt der Eintrag korrekt „alles sperren" und ist zusammen mit der Erlaubnisliste ein legitimes Whitelist-Only-Muster. Eine Meldung, die auch das anmeckert, erzieht den Betreiber dazu, Meldungen zu überlesen.

Table of Contents

Constants

CREATIONELL_CAPTCHA_HARDENING_DISMISS_ARG  = 'creationell_captcha_hardening_dismiss'
Query-Parameter des Ausblenden-Links.
CREATIONELL_CAPTCHA_HARDENING_DISMISS_META  = 'creationell_captcha_hardening_dismissed'
User-Meta-Schlüssel, unter dem ein Benutzer den Hinweis wegklickt.

Functions

creationell_captcha_hardening_covered_family()  : string
Ermittelt, WELCHE Adressfamilie ein gespeicherter Listeneintrag vollständig abdeckt und die Liste für diese Familie damit als Auswahl aufhebt.
creationell_captcha_hardening_mapped_ipv4_cidr()  : string
Erkennt einen CIDR-Bereich, der in IPv4-mapped Schreibweise notiert ist (`::ffff:0:0/96`, `::ffff:203.0.113.0/120`), und liefert seine gewöhnliche IPv4-Schreibweise zurück.
creationell_captcha_hardening_is_catch_all_pattern()  : bool
Prüft, ob ein `bypass_ua_allow`-Muster jeden Browser-Kennzeichner trifft.
creationell_captcha_hardening_matches_every_user_agent()  : bool
Prüft, ob ein `bypass_ua_allow`-Muster jeden realistischen User-Agent trifft, ohne vom Catch-all-Guard aussortiert zu werden (Befund B-M2).
creationell_captcha_hardening_scan()  : array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>
Durchsucht einen Satz Einstellungen nach gefährlicher Bestandskonfiguration.
creationell_captcha_hardening_findings()  : array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>
Führt den Scan gegen die aktuell gespeicherten Einstellungen aus.
creationell_captcha_hardening_fingerprint()  : string
Kurzkennung des aktuellen Fundbildes.
creationell_captcha_hardening_dismiss_redirect_target()  : string
Baut das Ziel des Redirects nach dem Ausblenden-Klick: dieselbe Seite ohne den Ausblenden-Parameter und ohne die Nonce.
creationell_captcha_hardening_handle_dismiss()  : void
Nimmt den Ausblenden-Klick entgegen.
creationell_captcha_hardening_render_notice()  : void
Zeigt die gefundene Bestandskonfiguration als Admin-Hinweis.

Constants

CREATIONELL_CAPTCHA_HARDENING_DISMISS_ARG

Query-Parameter des Ausblenden-Links.

public mixed CREATIONELL_CAPTCHA_HARDENING_DISMISS_ARG = 'creationell_captcha_hardening_dismiss'

CREATIONELL_CAPTCHA_HARDENING_DISMISS_META

User-Meta-Schlüssel, unter dem ein Benutzer den Hinweis wegklickt.

public mixed CREATIONELL_CAPTCHA_HARDENING_DISMISS_META = 'creationell_captcha_hardening_dismissed'

Functions

creationell_captcha_hardening_covered_family()

Ermittelt, WELCHE Adressfamilie ein gespeicherter Listeneintrag vollständig abdeckt und die Liste für diese Familie damit als Auswahl aufhebt.

creationell_captcha_hardening_covered_family(string $entry) : string

Zwei Fragen, zwei vorhandene Wurzeln — hier entsteht bewusst keine dritte IP-Prüfung (die Doppelung eines Validators war in diesem Audit schon zweimal ein Befund, AF-5 und CLI-1):

  1. creationell_captcha_is_valid_ip_or_cidr() — was die Eingabeprüfung dieser Version akzeptiert, ist per Definition kein Altlastwert. Das hält Falschmeldungen auf regulären Einträgen von vornherein fern, auch auf 0.0.0.0/1 + 128.0.0.0/1, mit denen ein Betreiber den gesamten Adressraum weiterhin abdecken darf — wenn er es ausspricht.
  2. creationell_captcha_ip_in_cidr() — derselbe Matcher, den Firewall, Proxy-Vertrauen und Watch-Liste zur Laufzeit benutzen. Trifft er die erste UND die letzte Adresse einer Familie, deckt der Eintrag alles dazwischen ebenfalls ab (CIDR-Präfixe sind zusammenhängend).

Deshalb erkennt die Prüfung auch Schreibweisen, die ein Textvergleich gegen „0.0.0.0/0" verfehlt hätte — 10.0.0.0/0 und 0.0.0.0/00 gehören dazu.

Zurückgegeben wird die Familie und nicht nur ein Ja/Nein, weil die Meldung sie braucht: 0.0.0.0/0 deckt AUSSCHLIESSLICH IPv4 ab, ::/0 ausschließlich IPv6. creationell_captcha_ip_in_cidr() verlangt gleiche Binärlänge von Adresse und Netz, ein Eintrag kann also nie beide Familien treffen — und was er nicht trifft, läuft an der Liste vorbei. Bei den vertrauenswürdigen Proxies ist genau das der gefährlichere Teil des Fundes (siehe die Meldung unten), deshalb darf die Familie hier nicht verlorengehen.

Parameters
$entry : string

Eintrag aus der gespeicherten Liste (bereits getrimmt).

Return values
string —

'IPv4', 'IPv6' oder '' — Letzteres heißt: deckt keine Familie vollständig ab, also kein Fund.

creationell_captcha_hardening_mapped_ipv4_cidr()

Erkennt einen CIDR-Bereich, der in IPv4-mapped Schreibweise notiert ist (`::ffff:0:0/96`, `::ffff:203.0.113.0/120`), und liefert seine gewöhnliche IPv4-Schreibweise zurück.

creationell_captcha_hardening_mapped_ipv4_cidr(string $entry) : string

WARUM DAS EINE EIGENE PRÜFUNG IST

Die Prüfung eine Etage höher (creationell_captcha_hardening_covered_family()) kann diese Klasse konstruktionsbedingt nicht finden. Sie steigt bei jedem Eintrag aus, den creationell_captcha_is_valid_ip_or_cidr() annimmt — und ::ffff:0:0/96 ist eine syntaktisch einwandfreie IPv6-Notation mit Präfixlänge 96, wird also angenommen. Auch die beiden Familien-Sonden greifen nicht: 0.0.0.0 ist vier Byte lang, der Eintrag sechzehn, und creationell_captcha_ip_in_cidr() verlangt zu Recht gleiche Binärlänge.

WAS DER EINTRAG BEDEUTET — GEMESSEN, NICHT VERMUTET

Bis 1.0.2 hat niemand die Client-Adresse vereinheitlicht. Auf einem Server, dessen PHP die Gegenstelle in mapped Schreibweise sieht (nginx mit ipv6only=off, Apache auf einem IPv6-Socket, viele Container-Setups), kam REMOTE_ADDR dort als ::ffff:203.0.113.7 an — sechzehn Byte, dieselbe Familie wie der Eintrag. ::ffff:0:0/96 traf damit JEDE IPv4-Adresse. In firewall_trusted_proxies hiess das: jede Gegenstelle gilt als eigener Proxy, der weitergeleitete Header wird ungeprüft übernommen. Die /0-Regel der Eingabeprüfung fängt diese Schreibweise nicht.

Seit Modul 27 (Befund I2) kanonisiert creationell_captcha_normalize_ip() die Client-Adresse auf die gewöhnliche IPv4-Schreibweise — vier Byte. Der RANGE bleibt bewusst stehen wie geschrieben, weil ein automatisch umgeschriebenes Vertrauens-Netz die eine Richtung wäre, die hier nie passieren darf. Folge: der Eintrag trifft heute NICHTS mehr. Gemessen:

ip_in_list( '203.0.113.7',        [ '::ffff:0:0/96' ] )  → false
ip_in_list( '::ffff:203.0.113.7', [ '::ffff:0:0/96' ] )  → false

Er ist damit kein offenes Tor mehr, sondern eine Zeile, die für den Betreiber wie eine wirksame Regel aussieht und keine ist — genau die Klasse unwirksam, für die dieses Feld existiert. Bei einer Blockliste ist das ein still verlorener Schutz, bei den Erlaubnis-/Vertrauenslisten eine still verlorene Ausnahme. Beides gehört gemeldet.

Blanke Einzeladressen in mapped Schreibweise sind NICHT betroffen: creationell_captcha_ip_in_list() kanonisiert dort beide Seiten, ein gespeichertes ::ffff:203.0.113.7 trifft also weiterhin. Deshalb verlangt diese Funktion einen Schrägstrich.

Präfixe unter 96 gelten nicht als mapped Bereich: dort tragen die zwölf Bytes des Mapped-Präfixes die Entscheidung nicht mehr vollständig, der Eintrag ist dann ein gewöhnlicher IPv6-Bereich (und ::ffff:0:0/0 fängt bereits die Familien-Prüfung oben ab).

Parameters
$entry : string

Eintrag aus der gespeicherten Liste (bereits getrimmt).

Return values
string —

Gewöhnliche IPv4-Schreibweise des Bereichs, oder '' wenn der Eintrag kein IPv4-Bereich in mapped Notation ist.

creationell_captcha_hardening_is_catch_all_pattern()

Prüft, ob ein `bypass_ua_allow`-Muster jeden Browser-Kennzeichner trifft.

creationell_captcha_hardening_is_catch_all_pattern(string $pattern) : bool

Entschieden wird das vom Produktions-Matcher selbst, nicht von einer zweiten Regel: creationell_captcha_wildcard_match() kennt den Schalter $allow_catch_all, und die Bypass-Auswertung ruft ihn mit false auf (BK-9). Ein Muster, das MIT erlaubtem Catch-all trifft und OHNE nicht mehr, ist genau eines, das der Schalter aussortiert — was immer die Regel dahinter gerade ist. Ein Muster wie *pingdom* trifft in beiden Aufrufen gleich und fällt damit nicht auf.

Ein Catch-all trifft jeden nicht-leeren Kennzeichner, deshalb genügt eine Probe.

GRENZE DIESER PRÜFUNG (Befund B-M2)

Das Kriterium findet definitionsgemäss nur die Menge, die der Laufzeit-Guard bereits aussortiert — also Muster mit wirkung: unwirksam. Ein Muster, das jeden realen Kennzeichner trifft, ohne ein blosses Sternchen zu sein, kommt durch beide Aufrufe gleich zurück und fällt hier NICHT auf. Das genannte Gegenbeispiel *pingdom* ist harmlos; Stern-Schraegstrich-Stern ist es nicht — dieses Muster trifft jeden User-Agent mit einem Schrägstrich, also praktisch jeden. Diese Klasse deckt creationell_captcha_hardening_matches_every_user_agent() ab.

Nebenwirkung: bei aktivem CREATIONELL_CAPTCHA_DEBUG schreibt der Matcher eine Zeile ins Log, wenn er ein Catch-all aussortiert — durch diese Prüfung also auch dann, wenn gerade keine echte Anfrage ausgewertet wird. Die Aussage der Zeile stimmt trotzdem: das Muster ist wirkungslos.

Parameters
$pattern : string

Muster aus der gespeicherten Liste.

Return values
bool

creationell_captcha_hardening_matches_every_user_agent()

Prüft, ob ein `bypass_ua_allow`-Muster jeden realistischen User-Agent trifft, ohne vom Catch-all-Guard aussortiert zu werden (Befund B-M2).

creationell_captcha_hardening_matches_every_user_agent(string $pattern) : bool

Der Guard in creationell_captcha_wildcard_match() verwirft ein Muster nur, wenn nach trim( $pattern, '*' ) nichts übrig bleibt — * und ** also, Stern-Schraegstrich-Stern nicht. Und dieses Muster trifft jeden User-Agent, der einen Schrägstrich enthält: Mozilla/5.0 …, curl/8.5.0, python-requests/2.31.0. Wer diese Zeile einträgt, hat den gesamten Schutz für jeden Besucher abgeschaltet und bekommt heute keine Meldung.

DAS KRITERIUM IST BEWUSST „ALLE SONDEN" UND NICHT „EINE SONDE"

Gemeldet wird nur, was JEDE der Sonden trifft — also ein Muster, das keinen realistischen Kennzeichner mehr auslässt. Ein Existenzquantor hätte Mozilla* (trifft Browser, nicht curl) und *Chrome* mitgemeldet; das sind enge Auswahlen, keine Totalabschaltungen, und eine Meldung darüber wäre genau die Falschmeldung, die den Betreiber zum Wegklicken erzieht. Die Sondenmenge deckt deshalb absichtlich verschiedene Bauformen ab: Browser mit Klammerausdruck, blankes Werkzeug ohne Leerzeichen, Bibliothek, Bot mit URL.

Anders als beim blossen Sternchen greift hier KEIN Laufzeit-Guard: das Muster wird ausgewertet und trifft. Der Fund ist deshalb aktiv, nicht unwirksam.

Parameters
$pattern : string

Muster aus der gespeicherten Liste.

Return values
bool

creationell_captcha_hardening_scan()

Durchsucht einen Satz Einstellungen nach gefährlicher Bestandskonfiguration.

creationell_captcha_hardening_scan(array<string, mixed> $settings[, bool|null $gd_available = null ]) : array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>

Keine Option wird gelesen, keine geschrieben. Der Aufrufer liefert die Einstellungen; das macht die Erkennung ohne WordPress prüfbar (tests/test-hardening-migration.php). Der einzige Blick nach draußen ist extension_loaded( 'gd' ) — und auch der ist über $gd_available abschaltbar, damit der Test nicht davon abhängt, wie der Rechner gebaut ist, auf dem er läuft.

Jeder Fund trägt ein Feld wirkung. Drei Werte, weil „greift nicht" drei verschiedene Dinge heißen kann und der Betreiber sie auseinanderhalten muss:

  • aktiv — der Eintrag greift zur Laufzeit hier und jetzt.
  • unwirksam — ein Laufzeit-Guard überspringt ihn seit dieser Version dauerhaft (BK-9, BK-14). Kein Schalter holt ihn zurück. Er steht trotzdem in der Konfiguration und liest sich für den Betreiber wie eine wirksame Ausnahme — deshalb genannt.
  • ruhend — der Eintrag greift nur nicht, weil die Funktion, die seine Liste liest, gerade ausgeschaltet ist. Er ist scharf, sobald sie eingeschaltet wird — eine Tretmine, kein toter Buchstabe.

Warum das Feld nicht pauschal aktiv sein darf: zwei der drei IP-Listen werden zur Laufzeit hinter einem Schalter gelesen (siehe unten). Auf einer Installation mit den Vorgabewerten (firewall_behind_proxy = false, code_challenge_enabled = false) bekäme der Betreiber sonst eine rote Fehlermeldung über zwei Optionswerte, die kein Codepfad überhaupt anschaut — und lernt daraus, Meldungen dieses Plugins wegzuklicken.

Parameters
$settings : array<string, mixed>

Gespeicherte Plugin-Einstellungen.

$gd_available : bool|null = null

PHP-GD vorhanden? null = selbst nachsehen. Nur zum Testen gesetzt — GD ist Teil des Watch-Listen-Gates (siehe creationell_captcha_should_issue_code_challenge()), aber eine Umgebungs- und keine Einstellungsfrage.

Return values
array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>

creationell_captcha_hardening_findings()

Führt den Scan gegen die aktuell gespeicherten Einstellungen aus.

creationell_captcha_hardening_findings() : array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>
Return values
array<int, array{id: string, liste: string, eintrag: string, wirkung: string, tab: string, meldung: string}>

creationell_captcha_hardening_fingerprint()

Kurzkennung des aktuellen Fundbildes.

creationell_captcha_hardening_fingerprint(array<int, array<string, string>> $findings) : string

Ein: id und wirkung. Nicht der Meldungstext — sonst ließe eine Umformulierung beim nächsten Update einen bereits weggeklickten Hinweis wieder auftauchen. wirkung muss dagegen einfließen: ein ruhend-Fund wird zu einem aktiv-Fund, sobald der Betreiber die zugehörige Funktion einschaltet, ohne dass sich die id ändert. Ohne dieses Feld bliebe die ausgeblendete Warnung auch dann ausgeblendet, wenn aus der Tretmine ein Loch geworden ist.

Umgekehrt bringt ein NEUER oder geänderter Fund den Hinweis zurück, obwohl der Benutzer den vorigen weggeklickt hat — genau das ist gewollt: ausgeblendet wird ein bestimmtes Fundbild, nicht die Meldung als solche.

Die id geht gehasht ein, nicht roh: sie enthält gespeicherte Listeneinträge und darf im Zusammenbau kein Trennzeichen zerschießen (ein bypass_cookies- Eintrag darf jedes Zeichen enthalten, auch Tabulator und Zeilenumbruch).

Parameters
$findings : array<int, array<string, string>>

Fundliste aus dem Scan.

Return values
string —

16 Hex-Zeichen, oder '' wenn es nichts zu melden gibt.

creationell_captcha_hardening_dismiss_redirect_target()

Baut das Ziel des Redirects nach dem Ausblenden-Klick: dieselbe Seite ohne den Ausblenden-Parameter und ohne die Nonce.

creationell_captcha_hardening_dismiss_redirect_target() : string

Wirkung des ursprünglichen Fundes war gering und ist es geblieben: Der Handler hängt hinter current_user_can( 'manage_options' ) und check_admin_referer(), ein Administrator müsste also selbst eine //-Schreibweise einer wp-admin-URL aufrufen, und wp_safe_redirect() fängt einen fremden Host ohnehin ab. Der Ausgang war der von N1 an der Under-Attack-Stelle beschriebene: statt zurück auf die Seite, von der er kam, landete der Administrator auf der wp-admin-Startseite — und der Hinweis, den er gerade weggeklickt hat, wäre dort erneut zu sehen.

Tags
since
1.1.0
Return values
string —

Relativer Ziel-URI für wp_safe_redirect().

creationell_captcha_hardening_handle_dismiss()

Nimmt den Ausblenden-Klick entgegen.

creationell_captcha_hardening_handle_dismiss() : void

Gespeichert wird die serverseitig NEU BERECHNETE Kennung, nicht die aus der URL. Sonst ließe sich mit einem präparierten Link ein Fundbild ausblenden, das es noch gar nicht gibt — der Hinweis wäre schon weggeklickt, bevor er das erste Mal fällig wird.

creationell_captcha_hardening_render_notice()

Zeigt die gefundene Bestandskonfiguration als Admin-Hinweis.

creationell_captcha_hardening_render_notice() : void

Auf allen Admin-Seiten, nicht nur auf den Plugin-Seiten: es geht um einen Schutz, der gerade nicht greift, und wer das erfahren soll, muss dafür nicht erst zufällig die Einstellungsseite öffnen. Damit der Hinweis trotzdem keine Tapete wird, ist er pro Benutzer ausblendbar (und kommt bei einem neuen Fund von selbst zurück, siehe creationell_captcha_hardening_fingerprint()).

Nur für Benutzer mit manage_options — niemand sonst könnte etwas ändern, und die Meldung nennt Details der Firewall-Konfiguration.


        
On this page

Search results