~/wordpress-plugins/plugin-absichern-nonces-escaping/

WordPress-Plugin absichern: Nonces, Escaping und Sanitizing

Rechteprüfung, Nonces, Sanitizing und Escaping: welche WordPress-Funktion wann greift, mit Beispielen für Formulare, AJAX, REST-Endpunkte und Datenbankabfragen.

Ein WordPress-Plugin sichern Sie mit vier Bausteinen ab: Rechte prüfen mit current_user_can(), Absicht prüfen mit einer Nonce, Eingaben bereinigen mit Sanitizing-Funktionen und Ausgaben escapen unmittelbar vor der Ausgabe. Für Datenbankabfragen mit Variablen kommt $wpdb->prepare() hinzu. Wer diese Regeln konsequent umsetzt, verhindert den Großteil der Lücken, die in Plugins gemeldet werden: Cross-Site-Scripting, Cross-Site Request Forgery, SQL-Injection und fehlende Berechtigungsprüfungen.

Warum Plugin-Sicherheit so wichtig ist

Plugins laufen mit allen Rechten der WordPress-Installation. Sie können Beiträge löschen, Benutzer anlegen und die Datenbank lesen. Eine Lücke in einem Plugin ist deshalb eine Lücke in jeder Website, die es nutzt. Angreifer durchsuchen das Netz automatisiert nach verwundbaren Plugin-Versionen, sobald eine Schwachstelle bekannt wird. Wie Sie als Betreiber Erweiterungen generell sicher auswählen, beschreibt der Bereich Erweiterungen sicher nutzen.

Die vier Bausteine im Überblick

Baustein Schützt vor Wichtige Funktionen Wann
Rechteprüfung unberechtigten Aktionen current_user_can() vor jeder Aktion
Nonce Cross-Site Request Forgery wp_nonce_field(), check_admin_referer(), check_ajax_referer() bei jeder zustandsändernden Anfrage
Sanitizing ungültigen und schädlichen Eingaben sanitize_text_field(), absint(), sanitize_email() beim Empfang
Escaping Cross-Site-Scripting esc_html(), esc_attr(), esc_url(), wp_kses_post() direkt bei der Ausgabe

1. Rechte prüfen

Prüfen Sie immer konkrete Fähigkeiten (Capabilities), nicht Rollen. Eine Rolle wie „Redakteur“ lässt sich umbenennen oder anpassen, eine Fähigkeit wie edit_posts beschreibt dagegen genau, was erlaubt ist.

if ( ! current_user_can( 'manage_options' ) ) {
    wp_die( esc_html__( 'Keine Berechtigung.', 'meinplugin' ), 403 );
}

Geht es um ein bestimmtes Objekt, übergeben Sie dessen ID. So greifen auch feinere Regeln, etwa dass Autoren nur eigene Beiträge bearbeiten dürfen:

if ( ! current_user_can( 'edit_post', $post_id ) ) {
    return;
}

is_admin() ist keine Rechteprüfung. Die Funktion sagt nur, ob gerade eine Seite des Adminbereichs geladen wird, und liefert auch bei AJAX-Anfragen über admin-ajax.php true, selbst für nicht angemeldete Besucher.

2. Nonces gegen CSRF

Bei Cross-Site Request Forgery bringt eine fremde Website den Browser eines angemeldeten Administrators dazu, unbemerkt eine Anfrage an Ihre Website zu senden. Eine Nonce ist ein Token, das an Benutzer, Aktion und Zeitfenster gebunden ist. Fehlt es oder passt es nicht, wird die Anfrage abgelehnt.

Im Formular:

<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>">
    <input type="hidden" name="action" value="meinplugin_speichern">
    <?php wp_nonce_field( 'meinplugin_speichern', 'meinplugin_nonce' ); ?>
    <input type="text" name="titel">
    <?php submit_button(); ?>
</form>

Bei der Verarbeitung:

add_action( 'admin_post_meinplugin_speichern', 'meinplugin_speichern' );

function meinplugin_speichern() {
    if ( ! current_user_can( 'manage_options' ) ) {
        wp_die( 'Keine Berechtigung.', 403 );
    }
    check_admin_referer( 'meinplugin_speichern', 'meinplugin_nonce' );

    $titel = isset( $_POST['titel'] )
        ? sanitize_text_field( wp_unslash( $_POST['titel'] ) )
        : '';

    update_option( 'meinplugin_titel', $titel );

    wp_safe_redirect( admin_url( 'options-general.php?page=meinplugin&gespeichert=1' ) );
    exit;
}

check_admin_referer() bricht bei ungültiger Nonce ab. Für AJAX nutzen Sie check_ajax_referer(), für eigene Prüfungen wp_verify_nonce(), das nur false oder einen Wert zurückgibt. Links mit Aktionen, etwa zum Löschen, versehen Sie mit wp_nonce_url().

Wichtig: Nonces gehören zu angemeldeten Benutzern. Für Formulare, die anonyme Besucher abschicken, schützen sie nur eingeschränkt, dort brauchen Sie zusätzlich Maßnahmen gegen Spam und Missbrauch.

3. Eingaben bereinigen

WordPress maskiert Daten aus $_POST, $_GET und $_COOKIE historisch mit Backslashes. Entfernen Sie diese mit wp_unslash() und bereinigen Sie danach passend zum erwarteten Typ:

Erwarteter Wert Funktion
einzeiliger Text sanitize_text_field()
mehrzeiliger Text sanitize_textarea_field()
ganze Zahl ab 0 absint()
E-Mail-Adresse sanitize_email()
Schlüssel, Slug, interne Kennung sanitize_key(), sanitize_title()
URL zum Speichern esc_url_raw() oder sanitize_url()
HTML wie in Beiträgen wp_kses_post()
Wert aus fester Liste Abgleich mit in_array( $wert, $erlaubt, true )

Bereinigen ist nicht dasselbe wie Validieren. Validieren prüft, ob ein Wert überhaupt zulässig ist, und lehnt ihn sonst ab. Bei festen Auswahlmöglichkeiten ist eine Positivliste die sicherste Variante.

4. Ausgaben escapen

Die wichtigste Regel lautet: so spät wie möglich escapen, also direkt beim echo. Dann sieht jeder Leser des Codes sofort, dass die Ausgabe sicher ist, und es spielt keine Rolle, woher der Wert stammt.

<h2><?php echo esc_html( $titel ); ?></h2>
<a href="<?php echo esc_url( $link ); ?>" title="<?php echo esc_attr( $hinweis ); ?>">
    <?php echo esc_html__( 'Mehr lesen', 'meinplugin' ); ?>
</a>
<div><?php echo wp_kses_post( $beschreibung ); ?></div>
Kontext Funktion
Text zwischen HTML-Tags esc_html()
Wert in einem HTML-Attribut esc_attr()
URL in href oder src esc_url()
Inhalt einer Textarea esc_textarea()
erlaubtes HTML wp_kses_post() oder wp_kses() mit eigener Positivliste
Daten für JavaScript wp_json_encode() oder wp_add_inline_script()
übersetzte Texte esc_html__(), esc_attr__(), esc_html_e()

Die Funktion muss zum Kontext passen. esc_html() in einem href-Attribut schützt zum Beispiel nicht vor javascript:-Links, dafür ist esc_url() zuständig.

Häufiger Irrtum

„Der Wert kommt doch aus meiner eigenen Option, der ist sicher.“ Optionen, Metadaten und Beitragstitel können von anderen Plugins, Importen oder kompromittierten Konten stammen. Escapen Sie deshalb jede Ausgabe, auch Werte, die Sie selbst gespeichert haben.

Sichere Datenbankabfragen

Für eigene SQL-Abfragen mit Variablen ist $wpdb->prepare() Pflicht. Es setzt Werte über Platzhalter sicher ein:

global $wpdb;
$tabelle = $wpdb->prefix . 'meinplugin_eintraege';

$zeilen = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT id, titel FROM {$tabelle} WHERE autor_id = %d AND status = %s",
        $autor_id,
        $status
    )
);

Die Platzhalter %d für Ganzzahlen, %f für Kommazahlen und %s für Zeichenketten werden automatisch korrekt maskiert. Neuere WordPress-Versionen kennen zusätzlich %i für Tabellen- und Spaltennamen. Setzen Sie Platzhalter nie selbst in Anführungszeichen. Für LIKE-Suchen maskieren Sie den Suchbegriff vorher mit $wpdb->esc_like(). Wo es geht, nutzen Sie ohnehin die WordPress-APIs wie WP_Query, get_posts() oder die Options- und Meta-Funktionen, die intern bereits sicher arbeiten.

AJAX und REST-Endpunkte

Viele Lücken stecken nicht in Formularen, sondern in Schnittstellen. Zwei Regeln:

  • AJAX: Aktionen, die mit wp_ajax_nopriv_ registriert sind, kann jeder Besucher aufrufen. Auch bei wp_ajax_ (nur angemeldet) prüfen Sie Rechte und Nonce, denn „angemeldet“ kann auch ein Abonnent sein.
  • REST API: Jede Route braucht einen permission_callback. Für wirklich öffentliche Endpunkte setzen Sie bewusst __return_true, für alle anderen eine echte Rechteprüfung.
register_rest_route( 'meinplugin/v1', '/eintrag/(?P<id>\d+)', array(
    'methods'             => 'POST',
    'callback'            => 'meinplugin_rest_speichern',
    'permission_callback' => function ( $request ) {
        return current_user_can( 'edit_post', (int) $request['id'] );
    },
    'args'                => array(
        'titel' => array( 'sanitize_callback' => 'sanitize_text_field' ),
    ),
) );

Die REST API prüft bei angemeldeten Benutzern zusätzlich eine eigene Nonce, die WordPress-Skripte automatisch mitsenden.

Weitere Schutzmaßnahmen

  • Direktaufruf verhindern: defined( 'ABSPATH' ) || exit; am Anfang jeder PHP-Datei.
  • Weiterleitungen absichern: wp_safe_redirect() statt wp_redirect() für Ziele, die aus Eingaben stammen.
  • Datei-Uploads prüfen: Dateityp mit wp_check_filetype_and_ext() kontrollieren und Uploads über wp_handle_upload() abwickeln.
  • Keine Geheimnisse im Code: API-Schlüssel gehören in Optionen oder Konstanten der wp-config.php, nicht ins Repository.
  • Regelmäßig prüfen: Werkzeuge wie der „Plugin Check“ und PHP_CodeSniffer mit den WordPress-Coding-Standards finden fehlendes Escaping und ungeprüfte Eingaben automatisch.

Checkliste für jede Funktion

  1. Wer darf das? current_user_can() mit passender Fähigkeit.
  2. Ist die Anfrage gewollt? Nonce prüfen.
  3. Was kommt herein? wp_unslash(), dann passend bereinigen oder validieren.
  4. Wie geht es in die Datenbank? WordPress-APIs oder $wpdb->prepare().
  5. Wie geht es hinaus? Kontextgerecht escapen, direkt bei der Ausgabe.

Den Aufbau eines Plugins, in dem diese Funktionen zum Einsatz kommen, zeigt der Artikel WordPress-Plugin erstellen. In welchen Hooks Formulare und Einstellungen verarbeitet werden, erklärt WordPress-Hooks erklärt.

faq

Fragen und Antworten

Ersetzt eine Nonce die Rechteprüfung?
Nein. Eine Nonce belegt nur, dass die Anfrage aus einer Seite stammt, die WordPress zuvor für diesen Benutzer und diese Aktion erzeugt hat. Ob die Person die Aktion ausführen darf, prüfen Sie zusätzlich mit current_user_can. Erst beides zusammen schützt zuverlässig.
Wie lange ist eine WordPress-Nonce gültig?
Standardmäßig bis zu 24 Stunden. WordPress rechnet in zwei Zeitfenstern zu je zwölf Stunden, eine Nonce aus dem aktuellen oder vorherigen Fenster wird akzeptiert. Anders als der Name vermuten lässt, kann sie in dieser Zeit mehrfach verwendet werden.
Reicht es, Eingaben zu bereinigen, wenn ich sie später ausgebe?
Nein. Bereinigen beim Eingang und Escapen bei der Ausgabe sind zwei getrennte Schutzschichten. Daten können auch aus anderen Quellen in die Datenbank gelangen, etwa durch andere Plugins oder Importe. Deshalb gilt: jede Ausgabe escapen, unabhängig davon, woher der Wert stammt.
Muss ich auch im Adminbereich escapen?
Ja. Auch im Adminbereich können manipulierte Daten Schadcode ausführen, und dort sind die Folgen oft schlimmer, weil Administratoren mit vollen Rechten angemeldet sind. Escaping gilt für jede Ausgabe, ob Frontend, Backend, E-Mail oder JSON-Antwort.