~/blog/wordpress-rest-authorisation-gapsPOSTED
von Shady Nathan Tawfik · 27. Juni 2026 · 4 min

Ein Plugin kann aktuell sein und trotzdem Datensätze an jeden ausgeben

Ein vollständig gepatchtes Content-Management-System trug ein Plugin mit aktuellen Releases und zwölf Befunden. Das Muster war Autorisierung: Routen, die antworteten, bevor der Berechtigungs-Callback lief.

Interaktiv[blog-2.0]

TL;DR: Eine vollständig gepatchte Content-Plattform gab trotzdem Datensätze an jeden aus, weil der Berechtigungs-Callback nach dem Handler lief. Ich zeige, wie ich die Routenliste gelesen und den per Parameter geschützten Schreibpfad gefunden habe.

Ausgangslage

Ich habe das Ziel als Black-Box geprüft: keine Zugangsdaten, nur die öffentlich sichtbare Fläche. Das ist, was ich sah, bevor ich etwas angefasst habe.

Eine Unternehmensberatung auf einem aktuellen Content-Management-Release mit einem Plugin-Ökosystem, betreut von einer externen Agentur. Geprüft wurden REST-Routen-Autorisierung, Benutzer-Enumeration, E-Mail-Authentifizierung und Content Security Policy der öffentlichen Oberfläche.

Ich habe alle identifizierenden Informationen entfernt. Das Ziel wird ausschließlich nach Branche und Technologieklasse beschrieben, damit das Muster übertragbar bleibt.

Das Muster

Das Muster, das ich erkannt habe — und das ich in ähnlichen Landschaften immer wieder finde:

Patch-Management beantwortet die Frage, welche Versionen als verwundbar bekannt sind. Es beantwortet nicht die Frage, ob die installierten Versionen das durchsetzen, was die Seite annimmt. Ein aktuelles Plugin mit zwölf Befunden ist kein Widerspruch, sondern das normale Ergebnis, wenn niemand die Berechtigungs-Callbacks von Routen liest, die niemand nutzt.

Der strukturelle Grund: REST-Endpunkte werden getrennt von den Seiten registriert, die auf sie verweisen. Ein Plugin-Autor registriert eine Route für ein internes Werkzeug, markiert sie als öffentlich, weil das Frontend die Daten braucht, und die administrative Variante nutzt dieselbe Route mit einem Parameter oder vergisst sie.

Der Meldung erscheint dadurch wie ein veralteter Zustand. Versions-Banner, Benutzer-Enumeration und ein offener Endpunkt lesen sich in einem Bericht wie Patch-Fehler. Sie sind es nicht, und die Behebung ist völlig anders: einmal Update, einmal eine Entscheidung, wer welche Zeilen sehen darf.

TL;DR: Ein vollständig gepatchtes Content-Management-System trug ein Plugin mit aktuellen Releases und zwölf Befunden. Das Muster war Autorisierung: Routen, die antworteten, bevor der Berechtigungs-Callback lief.

Befunde

SchweregradBefundNachweis
KRITISCHInterne REST-Route ohne Authentifizierung erreichbarInterne Reporting-Daten an anonyme Aufrufer zurückgegeben
HOCHBenutzer-Enumeration über die öffentliche REST-CollectionAutorenkonten und Kennungen aufgelistet
HOCHAdministrative Variante teilt sich eine öffentliche RouteSchreibpfad nur durch einen Request-Parameter geschützt
MITTELE-Mail-Authentifizierung nicht erzwungenKeine Enforcement-Policy für die Absenderdomain
MITTELContent Security Policy fehlt auf der HauptoberflächeKeine Policy auf einer einzigen First-Party-Seite
MITTELServicekennungen im SeitenquelltextNicht-Produktions-Endpunkt in Kommentaren benannt

Der Angriffspfad

Anonymous requestPublic REST routeHandler answers firstPermission callback runs laterData already sentWrite path reachable via parameter

Reproduktion im Labor

Jeder Befehl unten zielt auf einen Lab-Container, den ich selbst kontrolliere. Nichts davon ist auf ein laufendes System gerichtet.

Die Routen einer aktuellen Installation enumerieren

~/code/bash bash
# Ziel: Ihre eigene Laborinstanz
$ curl -s http://localhost:8080/wp-json/ | python3 -c '
import json,sys
d = json.load(sys.stdin)
for ns in d["namespaces"]:
    print(ns)'

# jeder Namespace liefert eine Routenliste; lesen
$ curl -s http://localhost:8080/wp-json/reporting/v1/ | python3 -m json.tool | head -40

# jetzt dieselbe Route ohne Zugangsdaten abfragen
$ curl -s -o /dev/null -w '%{http_code}\n' 'http://localhost:8080/wp-json/reporting/v1/entries'
$ curl -s 'http://localhost:8080/wp-json/reporting/v1/entries' | head -c 300

Erkennung in eigener Infrastruktur

Routen finden, die ohne Berechtigungsprüfung antworten

~/code/bash bash
$ curl -s https://example.org/wp-json/ | python3 -c '
import json,sys
d = json.load(sys.stdin)
for ns in d["namespaces"]:
    print(ns)' | while read -r ns; do
      curl -s "https://example.org/wp-json/$ns" | python3 -c '
import json,sys
try:
    routes = json.load(sys.stdin).get("routes", {})
except Exception:
    raise SystemExit
for r, v in routes.items():
    methods = [m for m in v.get("methods", []) if m != "OPTIONS"]
    if methods:
        print(",".join(methods), r)'
done | sort | uniq

Die E-Mail-Authentifizierungskette der Absenderdomain prüfen

~/code/bash bash
$ dig +short TXT example.org | grep -i spf
$ dig +short TXT _dmarc.example.org

Behebung

Vor dem Handler geschlossen aussteigen

~/code/php php
// jede Route antwortet erst, nachdem der Berechtigungs-Callback erfolgreich war
add_action( 'rest_api_init', function () {
    register_rest_route( 'reporting/v1', '/entries', array(
        'methods'             => WP_REST_Server::READABLE,
        'callback'            => 'reporting_entries',
        'permission_callback' => function ( $request ) {
            $user = wp_get_current_user();
            if ( ! $user || ! current_user_can( 'read_reporting' ) ) {
                return new WP_Error( 'forbidden', 'Nicht erlaubt.', array( 'status' => 403 ) );
            }
            return true;
        },
        'args'                => array( 'tenant' => array( 'validate_callback' => 'reporting_validate_tenant' ) ),
    ) );
});

// und den Mandanten niemals allein aus dem Request übernehmen
function reporting_validate_tenant( $value, $request, $key ) {
    $allowed = reporting_tenants_for_current_user();
    return in_array( (int) $value, $allowed, true ) ? $value
        : new WP_Error( 'invalid_tenant', 'Nicht Ihr Mandant.', array( 'status' => 403 ) );
}

Sobald ein Angreifer Zugriff hat, kann er folgende Aktionen durchführen:

Interne REST-Route ohne Authentifizierung erreichbar
Benutzer-Enumeration über die öffentliche REST-Collection
Administrative Variante teilt sich eine öffentliche Route
E-Mail-Authentifizierung nicht erzwungen
Content Security Policy fehlt auf der Hauptoberfläche
Servicekennungen im Seitenquelltext

Fazit — was ich daraus mitnehme

  • Ein aktuelles Plugin ist kein autorisiertes Plugin.
  • Lesen Sie die Berechtigungs-Callbacks von Routen, auf die niemand verweist.
  • Trennen Sie Lese- und Schreibpfade; ein Parameter ist keine Berechtigung.
  • Versions-Banner sehen wie Patch-Fehler aus und erzeugen die falsche Behebung.

Nur zu Bildungszwecken. Alle reproduzierbaren Befehle zielen auf meine eigene Lab-Umgebung (localhost), nie auf ein laufendes System. © 2026