{"id":20858,"date":"2026-10-05T14:18:00","date_gmt":"2026-10-05T12:18:00","guid":{"rendered":"https:\/\/www.lukaswojcik.com\/blog\/?p=20858"},"modified":"2026-09-30T12:13:32","modified_gmt":"2026-09-30T10:13:32","slug":"sanitizing-escaping-nonces-capabilities-plugin-review","status":"publish","type":"post","link":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/","title":{"rendered":"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review"},"content":{"rendered":"<p>Wer ein Plugin im Verzeichnis auf WordPress.org einreicht, bekommt bei Sicherheitsm\u00e4ngeln selten exotische Befunde zur\u00fcck. Die Seite \u201eCommon issues\u201c des Plugin-Review-Teams beginnt mit dem Abschnitt \u201eSecurity\u201c, und dort geht es um Grundlagen: Eingaben werden nicht bereinigt, Ausgaben nicht escaped, Nonce-Werte unbereinigt weitergereicht, SQL entsteht ohne <code>prepare()<\/code>. Das Security-Kapitel des Developer Handbook erg\u00e4nzt Nonces und Capabilities. Gerade der letzte Punkt f\u00e4llt leicht durch: Ein Formular-Handler pr\u00fcft die Nonce, aber nicht, ob der angemeldete Nutzer die Aktion \u00fcberhaupt ausf\u00fchren darf, und die Security-Pr\u00fcfungen von Plugin Check melden das nicht.<\/p>\n<p>Dieser Beitrag geht die vier Befunde an einem kleinen Admin-Plugin durch. Version 0.1 enth\u00e4lt alle Fehler zugleich, jede folgende Version behebt einen davon; zu jedem Befund stehen der fehlerhafte und der korrigierte Code nebeneinander. Am Ende kommt der REST-Teil dazu, denn dort hei\u00dft die Berechtigungspr\u00fcfung <code>permission_callback<\/code> und ist seit WordPress 5.5 formal Pflicht. Alle Funktionsbeschreibungen stammen aus dem Developer Handbook und der Code Reference auf developer.wordpress.org, Stand WordPress 7.1.2.<\/p>\n<h2>Die Regel: sanitize early, escape late, always validate<\/h2>\n<p>Die \u201eCommon issues\u201c fassen die Anforderung in einem Satz zusammen: \u201eSanitize early, Escape Late, Always Validate\u201c. Das Security-Kapitel im Common APIs Handbook formuliert dieselbe Haltung als Leits\u00e4tze, darunter \u201eNever trust user input\u201c, \u201eEscape as late as possible\u201c und \u201eEscape everything from untrusted sources\u201c; ausdr\u00fccklich z\u00e4hlt die Datenbank zu diesen unvertrauten Quellen. Ein weiterer Leitsatz lautet sinngem\u00e4\u00df: Bereinigen ist in Ordnung, Validieren und Zur\u00fcckweisen ist besser.<\/p>\n<p>Die drei Begriffe bezeichnen verschiedene Handgriffe. <strong>Validieren<\/strong> pr\u00fcft Daten gegen ein festes Muster mit eindeutigem Ergebnis, g\u00fcltig oder ung\u00fcltig; das Handbook empfiehlt Allowlists, verglichen mit <code>in_array()<\/code> im strikten Modus. <strong>Bereinigen<\/strong> (Sanitizing) filtert eine Eingabe so, dass sie gespeichert werden kann, etwa mit <code>sanitize_text_field()<\/code>. <strong>Escapen<\/strong> macht einen Wert f\u00fcr genau einen Ausgabekontext unsch\u00e4dlich, also HTML-Text, Attribut, URL oder Textarea. Die \u201eCommon issues\u201c betonen, dass sich die Funktionsgruppen nicht gegenseitig ersetzen: Escape-Funktionen taugen nicht zum Bereinigen, Sanitize-Funktionen nicht zum Escapen.<\/p>\n<p>Vor alle drei Stufen geh\u00f6rt bei schreibenden Anfragen eine Torpr\u00fcfung: Darf dieser Nutzer das (Capability), und kommt die Anfrage aus dem eigenen Formular (Nonce)? Das Diagramm zeigt den Weg eines Werts mit den Funktionen je Stufe.<\/p>\n<figure class=\"lw-diagram\">\n<img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/diagrams\/wp-sicherheitsbefunde-de.png?v=20260930\" width=\"1120\" height=\"580\" alt=\"Ein Wert, sechs Stufen: von der Anfrage bis zur escapten Ausgabe\"><figcaption>Erst das Tor, dann fr\u00fch bereinigen und pr\u00fcfen, mit Platzhaltern speichern, sp\u00e4t und kontextgerecht escapen<\/figcaption><\/figure>\n<h2>Das Beispiel-Plugin in Version 0.1<\/h2>\n<p>Das Plugin \u201eLW Notice Box\u201c legt unter \u201eEinstellungen\u201c eine Seite an. Dort lassen sich eine Beschriftung, ein Link, eine Nachricht mit HTML und die Zahl der angezeigten Protokollzeilen speichern. Jede \u00c4nderung landet als Zeile in einer eigenen Tabelle, ein Shortcode gibt den Hinweis im Frontend aus, und eine REST-Route erlaubt das \u00c4ndern der Beschriftung. Die Datei ist vollst\u00e4ndig und l\u00e4uft, ist aber absichtlich unsicher:<\/p>\n<pre><code>&lt;?php\n\/**\n * Plugin Name:       LW Notice Box\n * Description:       Demo plugin for the security review. Version 0.1 contains deliberate flaws.\n * Version:           0.1.0\n * Requires at least: 6.2\n * Requires PHP:      7.4\n * License:           GPL-2.0-or-later\n * Text Domain:       lw-notice-box\n *\/\n\nif ( ! defined( 'ABSPATH' ) ) {\n\texit;\n}\n\nregister_activation_hook( __FILE__, 'lw_nb_install' );\n\n\/**\n * Creates the log table (unchanged in all versions).\n *\/\nfunction lw_nb_install() {\n\tglobal $wpdb;\n\trequire_once ABSPATH . 'wp-admin\/includes\/upgrade.php';\n\n\t$table   = $wpdb-&gt;prefix . 'lw_nb_log';\n\t$charset = $wpdb-&gt;get_charset_collate();\n\n\tdbDelta(\n\t\t\"CREATE TABLE $table (\n\t\t\tid bigint(20) unsigned NOT NULL AUTO_INCREMENT,\n\t\t\tuser_id bigint(20) unsigned NOT NULL DEFAULT 0,\n\t\t\tlabel varchar(100) NOT NULL DEFAULT '',\n\t\t\tsaved_at datetime NOT NULL,\n\t\t\tPRIMARY KEY  (id)\n\t\t) $charset;\"\n\t);\n}\n\n\/**\n * Default values of the stored option (unchanged in all versions).\n *\/\nfunction lw_nb_defaults() {\n\treturn array(\n\t\t'label'   =&gt; '',\n\t\t'link'    =&gt; '',\n\t\t'message' =&gt; '',\n\t\t'rows'    =&gt; 10,\n\t);\n}\n\nadd_action( 'admin_menu', 'lw_nb_menu' );\n\n\/**\n * Adds the settings page (unchanged in all versions).\n *\/\nfunction lw_nb_menu() {\n\tadd_options_page( 'Notice Box', 'Notice Box', 'manage_options', 'lw-notice-box', 'lw_nb_render_page' );\n}\n\n\/\/ FLAW (findings 1 and 2): $_GET value inside SQL, unescaped output.\nfunction lw_nb_render_page() {\n\tglobal $wpdb;\n\t$opt     = wp_parse_args( get_option( 'lw_nb_notice', array() ), lw_nb_defaults() );\n\t$orderby = isset( $_GET['orderby'] ) ? $_GET['orderby'] : 'saved_at';\n\t$rows    = $wpdb-&gt;get_results( \"SELECT user_id, label, saved_at FROM {$wpdb-&gt;prefix}lw_nb_log ORDER BY $orderby DESC LIMIT {$opt['rows']}\" );\n\t?&gt;\n\t&lt;div class=\"wrap\"&gt;\n\t\t&lt;h1&gt;Notice Box&lt;\/h1&gt;\n\t\t&lt;form method=\"post\" action=\"&lt;?php echo admin_url( 'admin-post.php' ); ?&gt;\"&gt;\n\t\t\t&lt;input type=\"hidden\" name=\"action\" value=\"lw_nb_save\"&gt;\n\t\t\t&lt;p&gt;&lt;input type=\"text\" name=\"label\" value=\"&lt;?php echo $opt['label']; ?&gt;\"&gt;&lt;\/p&gt;\n\t\t\t&lt;p&gt;&lt;input type=\"url\" name=\"link\" value=\"&lt;?php echo $opt['link']; ?&gt;\"&gt;&lt;\/p&gt;\n\t\t\t&lt;p&gt;&lt;textarea name=\"message\"&gt;&lt;?php echo $opt['message']; ?&gt;&lt;\/textarea&gt;&lt;\/p&gt;\n\t\t\t&lt;p&gt;&lt;input type=\"number\" name=\"rows\" value=\"&lt;?php echo $opt['rows']; ?&gt;\"&gt;&lt;\/p&gt;\n\t\t\t&lt;p&gt;&lt;button type=\"submit\" class=\"button button-primary\"&gt;Save&lt;\/button&gt;&lt;\/p&gt;\n\t\t&lt;\/form&gt;\n\t\t&lt;ul&gt;\n\t\t\t&lt;?php foreach ( $rows as $row ) : ?&gt;\n\t\t\t\t&lt;li&gt;&lt;?php echo $row-&gt;saved_at . ' | ' . $row-&gt;label; ?&gt;&lt;\/li&gt;\n\t\t\t&lt;?php endforeach; ?&gt;\n\t\t&lt;\/ul&gt;\n\t&lt;\/div&gt;\n\t&lt;?php\n}\n\nadd_action( 'admin_post_lw_nb_save', 'lw_nb_save' );\n\n\/\/ FLAW (findings 1, 3 and 4): no capability, no nonce, raw $_POST, SQL by concatenation.\nfunction lw_nb_save() {\n\tglobal $wpdb;\n\tupdate_option( 'lw_nb_notice', $_POST );\n\t$wpdb-&gt;query( \"INSERT INTO {$wpdb-&gt;prefix}lw_nb_log (user_id, label, saved_at) VALUES (\" . get_current_user_id() . \", '\" . $_POST['label'] . \"', NOW())\" );\n\twp_redirect( admin_url( 'options-general.php?page=lw-notice-box' ) );\n\texit;\n}\n\nadd_shortcode( 'lw_notice', 'lw_nb_shortcode' );\n\n\/\/ FLAW (finding 2): stored values go to the front end unescaped.\nfunction lw_nb_shortcode() {\n\t$opt = wp_parse_args( get_option( 'lw_nb_notice', array() ), lw_nb_defaults() );\n\treturn '&lt;div class=\"lw-notice\"&gt;&lt;a href=\"' . $opt['link'] . '\"&gt;' . $opt['label'] . '&lt;\/a&gt;' . $opt['message'] . '&lt;\/div&gt;';\n}\n\nadd_action( 'rest_api_init', 'lw_nb_register_routes' );\n\n\/\/ FLAW (REST): no permission_callback, the route is public.\nfunction lw_nb_register_routes() {\n\tregister_rest_route(\n\t\t'lw-nb\/v1',\n\t\t'\/notice',\n\t\tarray(\n\t\t\t'methods'  =&gt; 'POST',\n\t\t\t'callback' =&gt; 'lw_nb_rest_update',\n\t\t)\n\t);\n}\n\nfunction lw_nb_rest_update( WP_REST_Request $request ) {\n\t$opt          = wp_parse_args( get_option( 'lw_nb_notice', array() ), lw_nb_defaults() );\n\t$opt['label'] = $request['label'];\n\tupdate_option( 'lw_nb_notice', $opt );\n\treturn rest_ensure_response( $opt );\n}<\/code><\/pre>\n<p>Die Kommentare mit \u201eFLAW\u201c markieren, wo welcher Befund steckt. Die Funktionen <code>lw_nb_install()<\/code>, <code>lw_nb_defaults()<\/code> und <code>lw_nb_menu()<\/code> bleiben in allen Versionen gleich; die \u00fcbrigen werden in den folgenden Abschnitten durch gleichnamige Fassungen ersetzt. Wer alle korrigierten Funktionen in die Datei \u00fcbernimmt und die Versionsnummer anhebt, erh\u00e4lt Version 1.0.<\/p>\n<h2>Befund 1: Eingaben ungepr\u00fcft \u00fcbernommen<\/h2>\n<p>Der Handler aus Version 0.1 schreibt das komplette <code>$_POST<\/code>-Array in die Option. Damit landen auch <code>action<\/code> und jedes beliebige Zusatzfeld in der Datenbank, und zwar noch mit den Backslashes, die WordPress beim Laden \u00fcber <code>wp_magic_quotes()<\/code> an <code>$_GET<\/code>, <code>$_POST<\/code>, <code>$_COOKIE<\/code> und <code>$_SERVER<\/code> anh\u00e4ngt. Die \u201eCommon issues\u201c raten ausdr\u00fccklich davon ab, den ganzen <code>$_POST<\/code>-, <code>$_REQUEST<\/code>&#8211; oder <code>$_GET<\/code>-Stapel zu verarbeiten.<\/p>\n<pre><code>\/\/ Version 0.1: the whole, still slashed $_POST array lands in the option,\n\/\/ the label is concatenated into SQL.\nupdate_option( 'lw_nb_notice', $_POST );\n$wpdb-&gt;query( \"INSERT INTO {$wpdb-&gt;prefix}lw_nb_log (user_id, label, saved_at) VALUES (\" . get_current_user_id() . \", '\" . $_POST['label'] . \"', NOW())\" );<\/code><\/pre>\n<p>Version 0.2 liest nur die vier ben\u00f6tigten Felder. Jedes davon durchl\u00e4uft zuerst <code>wp_unslash()<\/code>, das laut Code Reference Backslashes aus einer Zeichenkette oder rekursiv aus einem Array entfernt, und danach die passende Sanitize-Funktion. <code>sanitize_text_field()<\/code> pr\u00fcft auf ung\u00fcltiges UTF-8, wandelt einzelne <code>&lt;<\/code> in Entit\u00e4ten, entfernt alle Tags, Zeilenumbr\u00fcche, Tabs und \u00fcberz\u00e4hlige Leerzeichen. <code>sanitize_url()<\/code> bekommt eine Liste erlaubter Protokolle; <code>wp_kses_post()<\/code> l\u00e4sst nur das HTML stehen, das auch in Beitragsinhalten erlaubt ist. F\u00fcr die Zeilenzahl gen\u00fcgt <code>absint()<\/code>, das jeden Wert in eine nicht negative Ganzzahl umwandelt.<\/p>\n<pre><code>\/**\n * Handles the settings form (version 0.2: input is cleaned and validated).\n * Still missing: nonce (finding 3) and capability check (finding 4).\n *\/\nfunction lw_nb_save() {\n\t\/\/ Sanitize early: read only the fields we need, unslash, then clean.\n\t$label   = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : '';\n\t$link    = isset( $_POST['link'] ) ? sanitize_url( wp_unslash( $_POST['link'] ), array( 'http', 'https' ) ) : '';\n\t$message = isset( $_POST['message'] ) ? wp_kses_post( wp_unslash( $_POST['message'] ) ) : '';\n\t$rows    = isset( $_POST['rows'] ) ? absint( $_POST['rows'] ) : 10;\n\n\t\/\/ Always validate: reject what does not fit instead of storing it.\n\tif ( '' === $label || mb_strlen( $label ) &gt; 100 ) {\n\t\twp_die(\n\t\t\tesc_html__( 'The label must contain 1 to 100 characters.', 'lw-notice-box' ),\n\t\t\t'',\n\t\t\tarray( 'back_link' =&gt; true )\n\t\t);\n\t}\n\tif ( $rows &lt; 1 || $rows &gt; 50 ) {\n\t\t$rows = 10;\n\t}\n\n\tupdate_option(\n\t\t'lw_nb_notice',\n\t\tarray(\n\t\t\t'label'   =&gt; $label,\n\t\t\t'link'    =&gt; $link,\n\t\t\t'message' =&gt; $message,\n\t\t\t'rows'    =&gt; $rows,\n\t\t)\n\t);\n\tlw_nb_log_change( $label );\n\n\twp_safe_redirect( admin_url( 'options-general.php?page=lw-notice-box' ) );\n\texit;\n}\n\n\/**\n * Writes one log row. $wpdb-&gt;insert() prepares the values via the format list.\n *\n * @param string $label Already sanitized label.\n *\/\nfunction lw_nb_log_change( $label ) {\n\tglobal $wpdb;\n\n\t\/\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Custom table without a core API.\n\t$wpdb-&gt;insert(\n\t\t$wpdb-&gt;prefix . 'lw_nb_log',\n\t\tarray(\n\t\t\t'user_id'  =&gt; get_current_user_id(),\n\t\t\t'label'    =&gt; $label,\n\t\t\t'saved_at' =&gt; current_time( 'mysql' ),\n\t\t),\n\t\tarray( '%d', '%s', '%s' )\n\t);\n}<\/code><\/pre>\n<p>Nach dem Bereinigen folgt die Validierung. Eine leere oder zu lange Beschriftung wird nicht stillschweigend gek\u00fcrzt, sondern abgewiesen. Die Zeilenzahl muss zwischen 1 und 50 liegen, sonst gilt der Standardwert. Diese Grenzen sind fachliche Regeln des Plugins; keine Sanitize-Funktion kennt sie. Das Protokoll schreibt jetzt <code>$wpdb-&gt;insert()<\/code> mit einer Formatliste, die jeden Wert als Ganzzahl oder Zeichenkette ausweist.<\/p>\n<p>Der Kommentar mit <code>phpcs:ignore<\/code> geh\u00f6rt zu einer Warnung, die Plugin Check bei jedem direkten Datenbankzugriff ausgibt. Bei einer eigenen Tabelle gibt es keine Core-API, die Begr\u00fcndung steht deshalb im Kommentar. Wie Plugin Check solche Meldungen einordnet, behandelt <span class=\"lw-artikel-geplant\">ein eigener Beitrag dieser Reihe<\/span>.<\/p>\n<h3>Sonderfall SQL: $wpdb-&gt;prepare() mit %s, %d und %i<\/h3>\n<p>Bereinigen sch\u00fctzt nicht vor SQL-Injection. Die \u201eCommon issues\u201c verlangen bei eigenen Abfragen <code>wpdb<\/code>-Methoden zusammen mit <code>prepare()<\/code>. Der Fehler in Version 0.1 steckt in der Sortierung:<\/p>\n<pre><code>\/\/ Version 0.1: ?orderby=... goes straight into the query. ORDER BY needs no\n\/\/ quotes, so the slashes added by wp_magic_quotes() do not help here.\n$orderby = isset( $_GET['orderby'] ) ? $_GET['orderby'] : 'saved_at';\n$rows    = $wpdb-&gt;get_results( \"SELECT user_id, label, saved_at FROM {$wpdb-&gt;prefix}lw_nb_log ORDER BY $orderby DESC LIMIT {$opt['rows']}\" );<\/code><\/pre>\n<p>Der Parameter landet ohne Anf\u00fchrungszeichen in <code>ORDER BY<\/code>, die zus\u00e4tzlichen Backslashes von <code>wp_magic_quotes()<\/code> bewirken dort nichts. Laut Code Reference kennt <code>$wpdb-&gt;prepare()<\/code> die Platzhalter <code>%d<\/code> (Ganzzahl), <code>%f<\/code> (Gleitkommazahl), <code>%s<\/code> (Zeichenkette) und <code>%i<\/code> (Bezeichner wie Tabellen- oder Spaltennamen). <code>%i<\/code> kam mit WordPress 6.2 dazu; das Beispiel-Plugin nennt deshalb im Header \u201eRequires at least: 6.2\u201c. Alle Platzhalter bleiben ohne Anf\u00fchrungszeichen, zu jedem geh\u00f6rt genau ein Argument.<\/p>\n<pre><code>\/**\n * Returns the latest log rows.\n *\n * @param string $orderby Requested sort column (untrusted).\n * @param int    $limit   Number of rows.\n * @return array\n *\/\nfunction lw_nb_get_log( $orderby, $limit ) {\n\tglobal $wpdb;\n\n\t\/\/ An identifier placeholder quotes a name, it does not restrict it: allowlist first.\n\t$allowed = array( 'saved_at', 'label', 'user_id' );\n\tif ( ! in_array( $orderby, $allowed, true ) ) {\n\t\t$orderby = 'saved_at';\n\t}\n\n\t\/\/ phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table, small admin-only list.\n\treturn $wpdb-&gt;get_results(\n\t\t$wpdb-&gt;prepare(\n\t\t\t'SELECT user_id, label, saved_at FROM %i ORDER BY %i DESC LIMIT %d',\n\t\t\t$wpdb-&gt;prefix . 'lw_nb_log',\n\t\t\t$orderby,\n\t\t\tmin( 50, absint( $limit ) )\n\t\t)\n\t);\n}<\/code><\/pre>\n<p>Wichtig ist die Reihenfolge in dieser Funktion. <code>%i<\/code> setzt einen Namen korrekt in Backticks, beschr\u00e4nkt aber nicht, welcher Name ankommt; ohne Allowlist lie\u00dfe sich weiterhin nach jeder Spalte der Tabelle sortieren. Die Allowlist ist die Validierung, der Platzhalter die technische Absicherung. F\u00fcr <code>LIKE<\/code>-Suchen gilt eine weitere Regel aus der Code Reference: Das vollst\u00e4ndige Muster samt Prozentzeichen wird als Argument \u00fcbergeben und der Suchbegriff vorher mit <code>$wpdb-&gt;esc_like()<\/code> behandelt, etwa <code>$wpdb-&gt;prepare( 'SELECT COUNT(*) FROM %i WHERE label LIKE %s', $table, '%' . $wpdb-&gt;esc_like( $term ) . '%' )<\/code>. F\u00fcr Listen in <code>IN ( \u2026 )<\/code> zeigen die \u201eCommon issues\u201c ein Muster, das je Element einen eigenen Platzhalter erzeugt.<\/p>\n<h2>Befund 2: Ausgaben ohne Escaping<\/h2>\n<p>Version 0.1 gibt jeden gespeicherten Wert roh aus, im Admin-Formular wie im Shortcode. Enth\u00e4lt die Beschriftung ein Anf\u00fchrungszeichen, bricht sie aus dem <code>value<\/code>-Attribut aus; enth\u00e4lt die Nachricht ein <code>&lt;\/textarea&gt;<\/code>, endet das Textfeld vorzeitig. Die Bereinigung beim Speichern macht das Escaping nicht \u00fcberfl\u00fcssig, denn gespeicherte Werte k\u00f6nnen auch auf anderem Weg in die Datenbank gelangen, etwa \u00fcber einen Import, ein anderes Plugin oder eine \u00e4ltere Version des eigenen Codes.<\/p>\n<pre><code>\/\/ Version 0.1: attribute, textarea content and HTML block all printed raw.\n&lt;input type=\"text\" name=\"label\" value=\"&lt;?php echo $opt['label']; ?&gt;\"&gt;\n&lt;textarea name=\"message\"&gt;&lt;?php echo $opt['message']; ?&gt;&lt;\/textarea&gt;\nreturn '&lt;div class=\"lw-notice\"&gt;&lt;a href=\"' . $opt['link'] . '\"&gt;' . $opt['label'] . '&lt;\/a&gt;' . $opt['message'] . '&lt;\/div&gt;';<\/code><\/pre>\n<p>\u201eEscape late\u201c hei\u00dft: direkt beim <code>echo<\/code> und passend zum Kontext. Das Escaping-Kapitel des Handbooks nennt die Zuordnung, und die \u201eCommon issues\u201c verlangen, alle Variablen, Optionen und erzeugten Daten zum Zeitpunkt der Ausgabe zu escapen, nicht beim Zusammenbauen einer Variable.<\/p>\n<table>\n<thead>\n<tr>\n<th>Ausgabekontext<\/th>\n<th>Funktion<\/th>\n<th>Beispiel im Plugin<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Text zwischen HTML-Tags<\/td>\n<td><code>esc_html()<\/code><\/td>\n<td>Seitentitel, Protokollzeilen<\/td>\n<\/tr>\n<tr>\n<td>Wert eines HTML-Attributs<\/td>\n<td><code>esc_attr()<\/code><\/td>\n<td><code>value<\/code> von Beschriftung und Zeilenzahl<\/td>\n<\/tr>\n<tr>\n<td>URL in <code>href<\/code>, <code>src<\/code>, <code>action<\/code><\/td>\n<td><code>esc_url()<\/code><\/td>\n<td>Formularziel, Sortierlinks, Hinweislink<\/td>\n<\/tr>\n<tr>\n<td>Inhalt einer Textarea<\/td>\n<td><code>esc_textarea()<\/code><\/td>\n<td>Nachricht im Formular<\/td>\n<\/tr>\n<tr>\n<td>HTML, das erhalten bleiben soll<\/td>\n<td><code>wp_kses_post()<\/code><\/td>\n<td>Nachricht im Shortcode<\/td>\n<\/tr>\n<tr>\n<td>\u00dcbersetzter Text<\/td>\n<td><code>esc_html_e()<\/code>, <code>esc_attr__()<\/code><\/td>\n<td>Feldbeschriftungen<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<pre><code>\/**\n * Renders the settings page (version 1.0: every value escaped at output).\n *\/\nfunction lw_nb_render_page() {\n\t$opt = wp_parse_args( get_option( 'lw_nb_notice', array() ), lw_nb_defaults() );\n\n\t\/\/ Read-only sort parameter: it changes nothing and is checked in lw_nb_get_log().\n\t\/\/ phpcs:ignore WordPress.Security.NonceVerification.Recommended\n\t$orderby = isset( $_GET['orderby'] ) ? sanitize_key( wp_unslash( $_GET['orderby'] ) ) : 'saved_at';\n\t$rows    = lw_nb_get_log( $orderby, absint( $opt['rows'] ) );\n\t?&gt;\n\t&lt;div class=\"wrap\"&gt;\n\t\t&lt;h1&gt;&lt;?php echo esc_html( get_admin_page_title() ); ?&gt;&lt;\/h1&gt;\n\t\t&lt;form method=\"post\" action=\"&lt;?php echo esc_url( admin_url( 'admin-post.php' ) ); ?&gt;\"&gt;\n\t\t\t&lt;input type=\"hidden\" name=\"action\" value=\"lw_nb_save\"&gt;\n\t\t\t&lt;?php wp_nonce_field( 'lw_nb_save', 'lw_nb_nonce' ); \/\/ Finding 3. ?&gt;\n\t\t\t&lt;p&gt;\n\t\t\t\t&lt;label for=\"lw-nb-label\"&gt;&lt;?php esc_html_e( 'Label', 'lw-notice-box' ); ?&gt;&lt;\/label&gt;&lt;br&gt;\n\t\t\t\t&lt;input type=\"text\" id=\"lw-nb-label\" name=\"label\" class=\"regular-text\" value=\"&lt;?php echo esc_attr( $opt['label'] ); ?&gt;\"&gt;\n\t\t\t&lt;\/p&gt;\n\t\t\t&lt;p&gt;\n\t\t\t\t&lt;label for=\"lw-nb-link\"&gt;&lt;?php esc_html_e( 'Link', 'lw-notice-box' ); ?&gt;&lt;\/label&gt;&lt;br&gt;\n\t\t\t\t&lt;input type=\"url\" id=\"lw-nb-link\" name=\"link\" class=\"regular-text\" value=\"&lt;?php echo esc_url( $opt['link'] ); ?&gt;\"&gt;\n\t\t\t&lt;\/p&gt;\n\t\t\t&lt;p&gt;\n\t\t\t\t&lt;label for=\"lw-nb-message\"&gt;&lt;?php esc_html_e( 'Message (HTML allowed)', 'lw-notice-box' ); ?&gt;&lt;\/label&gt;&lt;br&gt;\n\t\t\t\t&lt;textarea id=\"lw-nb-message\" name=\"message\" rows=\"4\" class=\"large-text\"&gt;&lt;?php echo esc_textarea( $opt['message'] ); ?&gt;&lt;\/textarea&gt;\n\t\t\t&lt;\/p&gt;\n\t\t\t&lt;p&gt;\n\t\t\t\t&lt;label for=\"lw-nb-rows\"&gt;&lt;?php esc_html_e( 'Log rows (1-50)', 'lw-notice-box' ); ?&gt;&lt;\/label&gt;&lt;br&gt;\n\t\t\t\t&lt;input type=\"number\" id=\"lw-nb-rows\" name=\"rows\" min=\"1\" max=\"50\" value=\"&lt;?php echo esc_attr( $opt['rows'] ); ?&gt;\"&gt;\n\t\t\t&lt;\/p&gt;\n\t\t\t&lt;?php submit_button(); ?&gt;\n\t\t&lt;\/form&gt;\n\t\t&lt;p&gt;\n\t\t\t&lt;a href=\"&lt;?php echo esc_url( add_query_arg( 'orderby', 'saved_at' ) ); ?&gt;\"&gt;&lt;?php esc_html_e( 'Sort by date', 'lw-notice-box' ); ?&gt;&lt;\/a&gt; |\n\t\t\t&lt;a href=\"&lt;?php echo esc_url( add_query_arg( 'orderby', 'label' ) ); ?&gt;\"&gt;&lt;?php esc_html_e( 'Sort by label', 'lw-notice-box' ); ?&gt;&lt;\/a&gt;\n\t\t&lt;\/p&gt;\n\t\t&lt;ul&gt;\n\t\t\t&lt;?php foreach ( $rows as $row ) : ?&gt;\n\t\t\t\t&lt;li&gt;&lt;?php echo esc_html( $row-&gt;saved_at . ' | ' . $row-&gt;label ); ?&gt;&lt;\/li&gt;\n\t\t\t&lt;?php endforeach; ?&gt;\n\t\t&lt;\/ul&gt;\n\t&lt;\/div&gt;\n\t&lt;?php\n}\n\n\/**\n * Front-end output of the notice (version 1.0).\n *\n * @return string\n *\/\nfunction lw_nb_shortcode() {\n\t$opt = wp_parse_args( get_option( 'lw_nb_notice', array() ), lw_nb_defaults() );\n\tif ( '' === $opt['label'] ) {\n\t\treturn '';\n\t}\n\n\treturn sprintf(\n\t\t'&lt;div class=\"lw-notice\"&gt;&lt;a href=\"%1$s\"&gt;%2$s&lt;\/a&gt;%3$s&lt;\/div&gt;',\n\t\tesc_url( $opt['link'] ),\n\t\tesc_html( $opt['label'] ),\n\t\twp_kses_post( $opt['message'] )\n\t);\n}<\/code><\/pre>\n<p>Drei Details in dieser Fassung verdienen Beachtung. <code>esc_url()<\/code> liefert laut Code Reference eine leere Zeichenkette, wenn die URL ein Protokoll au\u00dferhalb der erlaubten Liste verwendet; ein gespeichertes <code>javascript:<\/code> wird so im Link nicht wirksam. <code>add_query_arg()<\/code> ohne dritten Parameter arbeitet mit der aktuellen Anfrage-URL und muss deshalb ebenfalls escaped werden. Und <code>esc_url_raw()<\/code> ist nach den \u201eCommon issues\u201c keine Escape-Funktion, sondern ein Sanitizer f\u00fcr Datenbank und Weiterleitung, vergleichbar mit <code>sanitize_url()<\/code>. Plugin Check pr\u00fcft diesen Befund mit dem Sniff <code>WordPress.Security.EscapeOutput<\/code>, allerdings nur bei direkter Ausgabe wie <code>echo<\/code> oder <code>printf()<\/code>: F\u00fcr Version 0.1 meldete er sieben Stellen in der Einstellungsseite, den unescapten R\u00fcckgabewert des Shortcodes dagegen nicht.<\/p>\n<h2>Befund 3: keine Nonce im Formular<\/h2>\n<p>Ohne Nonce kann jede fremde Website ein Formular bauen, das an <code>admin-post.php<\/code> sendet. \u00d6ffnet ein angemeldeter Administrator diese Seite, schickt sein Browser das Anmelde-Cookie mit, und die \u00c4nderung l\u00e4uft unter seinem Konto. Das Nonces-Kapitel beschreibt Nonces als Schutz gegen solche Angriffe, darunter CSRF; gegen Replay-Angriffe sch\u00fctzen sie nicht, weil WordPress nicht pr\u00fcft, ob eine Nonce schon benutzt wurde. Eine Nonce ist nach <code>wp_verify_nonce()<\/code> standardm\u00e4\u00dfig zwischen 12 und 24 Stunden g\u00fcltig. Der Quelltext von <code>wp_create_nonce()<\/code> bildet sie aus Aktion, Nutzer-ID, Session-Token und Zeitfenster.<\/p>\n<p>Die Korrektur besteht aus zwei Teilen. Das Formular gibt mit <code>wp_nonce_field( 'lw_nb_save', 'lw_nb_nonce' )<\/code> ein verstecktes Feld aus; die Zeile steht bereits in der Fassung von <code>lw_nb_render_page()<\/code> aus Befund 2. Der Handler pr\u00fcft das Feld mit <code>check_admin_referer()<\/code>, bevor er irgendetwas liest:<\/p>\n<pre><code>\/**\n * Handles the settings form (version 0.3: nonce checked).\n * Still missing: capability check (finding 4).\n *\/\nfunction lw_nb_save() {\n\t\/\/ Intent: did the request come from our own form? Dies with a 403 error page otherwise.\n\tcheck_admin_referer( 'lw_nb_save', 'lw_nb_nonce' );\n\n\t$label   = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : '';\n\t$link    = isset( $_POST['link'] ) ? sanitize_url( wp_unslash( $_POST['link'] ), array( 'http', 'https' ) ) : '';\n\t$message = isset( $_POST['message'] ) ? wp_kses_post( wp_unslash( $_POST['message'] ) ) : '';\n\t$rows    = isset( $_POST['rows'] ) ? absint( $_POST['rows'] ) : 10;\n\n\tif ( '' === $label || mb_strlen( $label ) &gt; 100 ) {\n\t\twp_die(\n\t\t\tesc_html__( 'The label must contain 1 to 100 characters.', 'lw-notice-box' ),\n\t\t\t'',\n\t\t\tarray( 'back_link' =&gt; true )\n\t\t);\n\t}\n\tif ( $rows &lt; 1 || $rows &gt; 50 ) {\n\t\t$rows = 10;\n\t}\n\n\tupdate_option(\n\t\t'lw_nb_notice',\n\t\tarray(\n\t\t\t'label'   =&gt; $label,\n\t\t\t'link'    =&gt; $link,\n\t\t\t'message' =&gt; $message,\n\t\t\t'rows'    =&gt; $rows,\n\t\t)\n\t);\n\tlw_nb_log_change( $label );\n\n\twp_safe_redirect( admin_url( 'options-general.php?page=lw-notice-box' ) );\n\texit;\n}<\/code><\/pre>\n<p><code>check_admin_referer()<\/code> beendet die Anfrage bei ung\u00fcltiger Nonce \u00fcber <code>wp_nonce_ays()<\/code> mit einer Fehlerseite (Status 403, \u201eThe link you followed has expired.\u201c); eine R\u00fcckfrage zeigt <code>wp_nonce_ays()<\/code> nur f\u00fcr die Aktion <code>log-out<\/code>. Ohne Aktionsnamen l\u00f6st der Aufruf seit WordPress 3.2.0 einen <code>_doing_it_wrong()<\/code>-Hinweis aus; ein eindeutiger Aktionsname je Formular ist deshalb Pflicht. Wo die Pr\u00fcfung von Hand mit <code>wp_verify_nonce()<\/code> geschieht, verlangen die \u201eCommon issues\u201c, das Feld vorher mit <code>wp_unslash()<\/code> und <code>sanitize_text_field()<\/code> zu behandeln, weil die Funktion pluggable ist: <code>wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['lw_nb_nonce'] ) ), 'lw_nb_save' )<\/code>.<\/p>\n<h2>Befund 4: keine Capability-Pr\u00fcfung, warum die Nonce nicht reicht<\/h2>\n<p>Version 0.3 wirkt sicher, ist es aber nicht. Die Code Reference beschreibt <code>check_admin_referer()<\/code> mit den Worten, die Funktion \u201everifies intent, not authorization\u201c und pr\u00fcfe die Capabilities des Nutzers nicht; daf\u00fcr sei <code>current_user_can()<\/code> zust\u00e4ndig. Das Nonces-Kapitel wird noch deutlicher: \u201eNonces should never be relied on for authentication, authorization, or access control.\u201c Funktionen seien mit <code>current_user_can()<\/code> zu sch\u00fctzen, und es sei stets anzunehmen, dass Nonces kompromittiert werden k\u00f6nnen.<\/p>\n<p>Im Beispiel wird das konkret. <code>add_options_page()<\/code> mit <code>manage_options<\/code> sch\u00fctzt nur die Anzeige der Einstellungsseite. Der Handler h\u00e4ngt dagegen an <code>admin_post_lw_nb_save<\/code>, und <code>admin-post.php<\/code> feuert diesen Hook laut Quelltext f\u00fcr jeden angemeldeten Nutzer, ohne eigene Rechtepr\u00fcfung. Eine Nonce h\u00e4ngt an Aktion, Nutzer und Sitzung. Gibt irgendein Teil des Plugins eine Nonce f\u00fcr <code>lw_nb_save<\/code> auch an Nutzer mit geringeren Rechten aus, etwa in einem Frontend-Formular, oder gelangt eine g\u00fcltige Nonce auf anderem Weg nach au\u00dfen, kann damit auch ein angemeldeter Abonnent die Einstellungen \u00e4ndern. Eine Nonce beantwortet die Frage \u201eWollte dieser Nutzer diese Anfrage aus diesem Formular senden?\u201c, nicht die Frage \u201eDarf er das?\u201c.<\/p>\n<p>Version 1.0 stellt deshalb die Capability-Pr\u00fcfung an den Anfang und beh\u00e4lt die Nonce als zweite, unabh\u00e4ngige Pr\u00fcfung:<\/p>\n<pre><code>\/**\n * Handles the settings form (version 1.0).\n *\/\nfunction lw_nb_save() {\n\t\/\/ Authorization: may this user change the settings at all?\n\tif ( ! current_user_can( 'manage_options' ) ) {\n\t\twp_die(\n\t\t\tesc_html__( 'You are not allowed to change these settings.', 'lw-notice-box' ),\n\t\t\t'',\n\t\t\tarray( 'response' =&gt; 403 )\n\t\t);\n\t}\n\n\t\/\/ Intent: did the request come from our own form?\n\tcheck_admin_referer( 'lw_nb_save', 'lw_nb_nonce' );\n\n\t\/\/ Sanitize early.\n\t$label   = isset( $_POST['label'] ) ? sanitize_text_field( wp_unslash( $_POST['label'] ) ) : '';\n\t$link    = isset( $_POST['link'] ) ? sanitize_url( wp_unslash( $_POST['link'] ), array( 'http', 'https' ) ) : '';\n\t$message = isset( $_POST['message'] ) ? wp_kses_post( wp_unslash( $_POST['message'] ) ) : '';\n\t$rows    = isset( $_POST['rows'] ) ? absint( $_POST['rows'] ) : 10;\n\n\t\/\/ Always validate.\n\tif ( '' === $label || mb_strlen( $label ) &gt; 100 ) {\n\t\twp_die(\n\t\t\tesc_html__( 'The label must contain 1 to 100 characters.', 'lw-notice-box' ),\n\t\t\t'',\n\t\t\tarray( 'back_link' =&gt; true )\n\t\t);\n\t}\n\tif ( $rows &lt; 1 || $rows &gt; 50 ) {\n\t\t$rows = 10;\n\t}\n\n\tupdate_option(\n\t\t'lw_nb_notice',\n\t\tarray(\n\t\t\t'label'   =&gt; $label,\n\t\t\t'link'    =&gt; $link,\n\t\t\t'message' =&gt; $message,\n\t\t\t'rows'    =&gt; $rows,\n\t\t)\n\t);\n\tlw_nb_log_change( $label );\n\n\twp_safe_redirect( admin_url( 'options-general.php?page=lw-notice-box' ) );\n\texit;\n}<\/code><\/pre>\n<p>Gepr\u00fcft wird eine Capability, keine Rolle. Die Code Reference zu <code>current_user_can()<\/code> nennt die Pr\u00fcfung auf Rollennamen \u201ediscouraged\u201c, weil sie unzuverl\u00e4ssige Ergebnisse liefern kann. F\u00fcr Aktionen an einzelnen Objekten gibt es Meta-Capabilities mit Objekt-ID, etwa <code>current_user_can( 'edit_post', $post_id )<\/code>, die WordPress \u00fcber <code>map_meta_cap()<\/code> auf primitive Capabilities abbildet. Plugin Check hilft bei diesem Befund wenig: Die f\u00fcnf Pr\u00fcfungen im Security-Ordner des Plugin-Check-Repositorys behandeln Datenbankzugriffe, Escaping, Nonces und Weiterleitungen, eine fehlende Capability-Pr\u00fcfung erkennt keine davon.<\/p>\n<h2>REST-Routen: permission_callback ist Pflicht<\/h2>\n<p>Bei REST-Routen verschiebt sich die Aufgabenteilung. Die Nonce \u00fcbernimmt der Core: Bei Cookie-Authentifizierung erwartet die REST-API eine Nonce mit der Aktion <code>wp_rest<\/code>, \u00fcbergeben als <code>_wpnonce<\/code> oder im Header <code>X-WP-Nonce<\/code>. Fehlt sie, setzt die API laut Handbook den aktuellen Nutzer auf 0, und die Anfrage gilt als nicht angemeldet. Die Berechtigung dagegen geh\u00f6rt in den <code>permission_callback<\/code> der Route. Version 0.1 l\u00e4sst ihn weg:<\/p>\n<pre><code>\/\/ Version 0.1: since WordPress 5.5 this raises a _doing_it_wrong() notice,\n\/\/ but the route is still registered - without any check, for everyone.\nregister_rest_route(\n\t'lw-nb\/v1',\n\t'\/notice',\n\tarray(\n\t\t'methods'  =&gt; 'POST',\n\t\t'callback' =&gt; 'lw_nb_rest_update',\n\t)\n);<\/code><\/pre>\n<p>Seit WordPress 5.5 meldet <code>register_rest_route()<\/code> in diesem Fall einen <code>_doing_it_wrong()<\/code>-Hinweis. Bei aktivem <code>WP_DEBUG<\/code> erh\u00e4lt eine REST-Anfrage ihn als Antwort-Header <code>X-WP-DoingItWrong<\/code>; in <code>debug.log<\/code> landet er nur, wenn die Routen au\u00dferhalb einer REST-Anfrage registriert werden, etwa beim Laden des Block-Editors oder in WP-CLI. Registriert wird die Route trotzdem, und der REST-Server ruft die Berechtigungspr\u00fcfung nur auf, wenn ein Callback gesetzt ist. Die Dev-Note zu 5.5 begr\u00fcndet den Hinweis damit, dass ein vergessener oder falsch geschriebener Callback eine Route unbeabsichtigt \u00f6ffentlich macht. F\u00fcr absichtlich \u00f6ffentliche Routen empfehlen Handbook und Hinweistext <code>__return_true<\/code>, damit die Absicht im Code steht.<\/p>\n<pre><code>\/**\n * Registers GET (public) and POST (administrators only) for \/lw-nb\/v1\/notice.\n *\/\nfunction lw_nb_register_routes() {\n\tregister_rest_route(\n\t\t'lw-nb\/v1',\n\t\t'\/notice',\n\t\tarray(\n\t\t\tarray(\n\t\t\t\t'methods'             =&gt; WP_REST_Server::READABLE,\n\t\t\t\t'callback'            =&gt; 'lw_nb_rest_get',\n\t\t\t\t'permission_callback' =&gt; '__return_true', \/\/ Public on purpose.\n\t\t\t),\n\t\t\tarray(\n\t\t\t\t'methods'             =&gt; WP_REST_Server::EDITABLE,\n\t\t\t\t'callback'            =&gt; 'lw_nb_rest_update',\n\t\t\t\t'permission_callback' =&gt; 'lw_nb_rest_can_edit',\n\t\t\t\t'args'                =&gt; array(\n\t\t\t\t\t'label' =&gt; array(\n\t\t\t\t\t\t'type'              =&gt; 'string',\n\t\t\t\t\t\t'required'          =&gt; true,\n\t\t\t\t\t\t'minLength'         =&gt; 1,\n\t\t\t\t\t\t'maxLength'         =&gt; 100,\n\t\t\t\t\t\t\/\/ A custom sanitize_callback replaces the default schema check,\n\t\t\t\t\t\t\/\/ so the validation callback is set explicitly.\n\t\t\t\t\t\t'validate_callback' =&gt; 'rest_validate_request_arg',\n\t\t\t\t\t\t'sanitize_callback' =&gt; 'sanitize_text_field',\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t)\n\t);\n}\n\n\/**\n * Authorization for write access.\n *\n * @return bool\n *\/\nfunction lw_nb_rest_can_edit() {\n\treturn current_user_can( 'manage_options' );\n}\n\n\/**\n * Public read access: only the fields the front end needs.\n *\n * @return WP_REST_Response\n *\/\nfunction lw_nb_rest_get() {\n\t$opt = wp_parse_args( get_option( 'lw_nb_notice', array() ), lw_nb_defaults() );\n\treturn rest_ensure_response(\n\t\tarray(\n\t\t\t'label' =&gt; $opt['label'],\n\t\t\t'link'  =&gt; $opt['link'],\n\t\t)\n\t);\n}\n\n\/**\n * Write access; runs only after lw_nb_rest_can_edit() returned true.\n *\n * @param WP_REST_Request $request Request with the sanitized label.\n * @return WP_REST_Response\n *\/\nfunction lw_nb_rest_update( WP_REST_Request $request ) {\n\t$opt          = wp_parse_args( get_option( 'lw_nb_notice', array() ), lw_nb_defaults() );\n\t$opt['label'] = $request-&gt;get_param( 'label' );\n\tupdate_option( 'lw_nb_notice', $opt );\n\tlw_nb_log_change( $opt['label'] );\n\n\treturn rest_ensure_response( array( 'label' =&gt; $opt['label'] ) );\n}<\/code><\/pre>\n<p>Gibt der Callback <code>false<\/code> zur\u00fcck, antwortet der Server mit dem Fehler <code>rest_forbidden<\/code>, und zwar mit Status 401 f\u00fcr nicht angemeldete und 403 f\u00fcr angemeldete Nutzer. Eine Falle steckt in der Argumentdefinition: Ist ein eigener <code>sanitize_callback<\/code> gesetzt, verwendet WordPress nicht mehr die Standardbehandlung <code>rest_parse_request_arg()<\/code>, die das Schema pr\u00fcft. Deshalb steht <code>rest_validate_request_arg<\/code> ausdr\u00fccklich als <code>validate_callback<\/code> in der Definition, sonst blieben <code>minLength<\/code> und <code>maxLength<\/code> wirkungslos. Wie sich REST-Endpunkte schnell und mit OAuth-2.0-Autorisierung bauen lassen, zeigen die Beitr\u00e4ge zu <a href=\"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/tutorials-de-wordpress-plugins-tricks-de\/hochperformante-eigene-rest-api-endpunkte-in-wordpress-entwickeln\/\">hochperformanten eigenen REST-Endpunkten<\/a> und zur <a href=\"https:\/\/www.lukaswojcik.com\/blog\/de\/web-entwicklung\/sichere-wordpress-rest-api-oauth-2-0-und-benutzerdefinierte-autorisierte\/\">sicheren REST-API mit autorisierten Endpunkten<\/a>.<\/p>\n<h2>Grenzen und offene Punkte<\/h2>\n<p>Der Code lief auf einer Testinstallation mit WordPress 7.1.2 und PHP 8.4, als Administrator, als Abonnent und ohne Anmeldung, und wurde mit Plugin Check 2.1.0 gepr\u00fcft. F\u00fcr Version 1.0 meldet Plugin Check au\u00dfer der fehlenden <code>readme.txt<\/code> nichts; ohne die <code>phpcs:ignore<\/code>-Kommentare blieben f\u00fcnf Warnungen: zweimal <code>NonceVerification.Recommended<\/code> f\u00fcr den Sortierparameter, <code>DirectQuery<\/code> f\u00fcr <code>$wpdb-&gt;insert()<\/code> und f\u00fcr die Log-Abfrage sowie <code>NoCaching<\/code> f\u00fcr die Log-Abfrage. F\u00fcr Version 0.1 listet er 10 Fehler und 17 Warnungen im Code, keine davon zur fehlenden Capability-Pr\u00fcfung oder zum fehlenden <code>permission_callback<\/code>.<\/p>\n<p>Einige Themen bleiben bewusst au\u00dfen vor: AJAX-Handler mit <code>check_ajax_referer()<\/code>, Datei-Uploads, eigene Rollen und Capabilities sowie Content Security Policy. Die vier Befunde decken die h\u00e4ufigen Grundlagen ab, ersetzen aber kein vollst\u00e4ndiges Sicherheitskonzept. Und die Allowlist f\u00fcr Sortierspalten ist eine Validierung, die nur so gut ist wie ihre Pflege; kommt eine Spalte hinzu, muss auch die Liste wachsen.<\/p>\n<div class=\"lw-faq\">\n<h2>Fragen und Antworten<\/h2>\n<h3>Braucht auch ein GET-Parameter, der nur die Sortierung \u00e4ndert, eine Nonce?<\/h3>\n<p>Nonces sch\u00fctzen Aktionen, die etwas ver\u00e4ndern. Ein Parameter wie <code>orderby<\/code>, der nur die Anzeige sortiert, \u00e4ndert keinen Zustand. Plugin Check meldet den Zugriff trotzdem als Warnung <code>WordPress.Security.NonceVerification.Recommended<\/code>; ein <code>phpcs:ignore<\/code>-Kommentar mit Begr\u00fcndung dokumentiert die Entscheidung. Validiert werden muss der Wert dennoch, im Beispiel \u00fcber eine Allowlist erlaubter Spalten.<\/p>\n<h3>Reicht sanitize_text_field() als Schutz gegen SQL-Injection?<\/h3>\n<p>Nein. <code>sanitize_text_field()<\/code> entfernt Tags, Zeilenumbr\u00fcche und ung\u00fcltiges UTF-8, macht einen Wert aber nicht SQL-sicher. Werte geh\u00f6ren als Platzhalter in <code>$wpdb-&gt;prepare()<\/code> oder als Formatliste in <code>$wpdb-&gt;insert()<\/code>. Spalten- und Tabellennamen brauchen seit WordPress 6.2 <code>%i<\/code> und zus\u00e4tzlich eine Allowlist.<\/p>\n<h3>Warum nicht einfach pr\u00fcfen, ob der Nutzer die Rolle \u201eadministrator\u201c hat?<\/h3>\n<p>Rollen lassen sich umbauen, Capabilities sind die eigentliche Rechteeinheit. Die Code Reference zu <code>current_user_can()<\/code> nennt die Pr\u00fcfung auf Rollennamen \u201ediscouraged\u201c, weil sie unzuverl\u00e4ssige Ergebnisse liefern kann. Zu beachten ist au\u00dferdem: F\u00fcr Super-Admins in einem Multisite-Netzwerk liefert <code>current_user_can()<\/code> immer <code>true<\/code>, sofern die Capability nicht ausdr\u00fccklich verweigert wird.<\/p>\n<h3>Muss eine eigene REST-Route die Nonce selbst pr\u00fcfen?<\/h3>\n<p>Bei Cookie-Authentifizierung pr\u00fcft der Core die Nonce mit der Aktion <code>wp_rest<\/code>. Fehlt sie, gilt die Anfrage als nicht angemeldet, der aktuelle Nutzer ist 0. Die Route selbst k\u00fcmmert sich um die Berechtigung im <code>permission_callback<\/code>, etwa mit <code>current_user_can( 'manage_options' )<\/code>; \u00f6ffentliche Routen setzen ausdr\u00fccklich <code>__return_true<\/code>.<\/p>\n<\/div>\n<div class=\"lw-quellen\">\n<h2>Quellen<\/h2>\n<p>Abgerufen am 29.09.2026.<\/p>\n<ul>\n<li><a href=\"https:\/\/developer.wordpress.org\/apis\/security\/\" target=\"_blank\" rel=\"noopener noreferrer\">Common APIs Handbook: Security<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/apis\/security\/sanitizing\/\" target=\"_blank\" rel=\"noopener noreferrer\">Common APIs Handbook: Sanitizing Data<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/apis\/security\/data-validation\/\" target=\"_blank\" rel=\"noopener noreferrer\">Common APIs Handbook: Data Validation<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/apis\/security\/escaping\/\" target=\"_blank\" rel=\"noopener noreferrer\">Common APIs Handbook: Escaping Data<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/apis\/security\/nonces\/\" target=\"_blank\" rel=\"noopener noreferrer\">Common APIs Handbook: Nonces<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/apis\/security\/user-roles-and-capabilities\/\" target=\"_blank\" rel=\"noopener noreferrer\">Common APIs Handbook: User Roles and Capabilities<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/plugins\/wordpress-org\/common-issues\/\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Handbook: Common issues (Plugin-Review-Team)<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/rest-api\/extending-the-rest-api\/adding-custom-endpoints\/\" target=\"_blank\" rel=\"noopener noreferrer\">REST API Handbook: Adding Custom Endpoints<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/rest-api\/using-the-rest-api\/authentication\/\" target=\"_blank\" rel=\"noopener noreferrer\">REST API Handbook: Authentication<\/a><\/li>\n<li><a href=\"https:\/\/make.wordpress.org\/core\/2020\/07\/22\/rest-api-changes-in-wordpress-5-5\/\" target=\"_blank\" rel=\"noopener noreferrer\">Make WordPress Core: REST API changes in WordPress 5.5<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/sanitize_text_field\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: sanitize_text_field()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/absint\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: absint()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_unslash\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: wp_unslash()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/esc_html\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: esc_html()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/esc_attr\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: esc_attr()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/esc_url\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: esc_url()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_kses_post\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: wp_kses_post()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_nonce_field\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: wp_nonce_field()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/check_admin_referer\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: check_admin_referer()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/wp_verify_nonce\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: wp_verify_nonce()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/current_user_can\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: current_user_can()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/classes\/wpdb\/prepare\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: wpdb::prepare()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/functions\/register_rest_route\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: register_rest_route()<\/a><\/li>\n<li><a href=\"https:\/\/developer.wordpress.org\/reference\/hooks\/admin_post_action\/\" target=\"_blank\" rel=\"noopener noreferrer\">Code Reference: admin_post_{$action}<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/trunk\/src\/wp-admin\/admin-post.php\" target=\"_blank\" rel=\"noopener noreferrer\">wordpress-develop: wp-admin\/admin-post.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/trunk\/src\/wp-includes\/pluggable.php\" target=\"_blank\" rel=\"noopener noreferrer\">wordpress-develop: wp-includes\/pluggable.php (wp_create_nonce, check_admin_referer)<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/trunk\/src\/wp-includes\/load.php\" target=\"_blank\" rel=\"noopener noreferrer\">wordpress-develop: wp-includes\/load.php (wp_magic_quotes)<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/trunk\/src\/wp-includes\/rest-api\/class-wp-rest-server.php\" target=\"_blank\" rel=\"noopener noreferrer\">wordpress-develop: class-wp-rest-server.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/wordpress-develop\/blob\/trunk\/src\/wp-includes\/rest-api\/class-wp-rest-request.php\" target=\"_blank\" rel=\"noopener noreferrer\">wordpress-develop: class-wp-rest-request.php<\/a><\/li>\n<li><a href=\"https:\/\/github.com\/WordPress\/plugin-check\/tree\/trunk\/includes\/Checker\/Checks\/Security\" target=\"_blank\" rel=\"noopener noreferrer\">Plugin Check: Security-Pr\u00fcfungen<\/a><\/li>\n<li><a href=\"https:\/\/wordpress.org\/download\/releases\/\" target=\"_blank\" rel=\"noopener noreferrer\">WordPress Releases (aktuell 7.1.2 vom 22.09.2026)<\/a><\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Vier Sicherheitsbefunde aus dem Plugin-Review von WordPress.org, jeweils an einem kleinen Admin-Plugin mit fehlerhaftem und korrigiertem Code, dazu permission_callback f\u00fcr REST-Routen.<\/p>\n","protected":false},"author":1,"featured_media":20895,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[24],"tags":[92274,92071,92277],"class_list":["post-20858","post","type-post","status-publish","format-standard","hentry","category-wordpress-plugins-tricks-de","tag-php-de","tag-web-security-de","tag-wordpress-de"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.1 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review | Lukas Wojcik<\/title>\n<meta name=\"description\" content=\"Sanitize early, escape late, always validate: fehlerhafter und korrigierter Code zu Eingaben, Ausgaben, Nonces, Capabilities und REST.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review | Lukas Wojcik\" \/>\n<meta property=\"og:description\" content=\"Sanitize early, escape late, always validate: fehlerhafter und korrigierter Code zu Eingaben, Ausgaben, Nonces, Capabilities und REST.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/\" \/>\n<meta property=\"og:site_name\" content=\"Lukas Wojcik - Blog\" \/>\n<meta property=\"article:published_time\" content=\"2026-10-05T12:18:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"630\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Lukas Wojcik\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Lukas Wojcik\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"25 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/\"},\"author\":{\"name\":\"Lukas Wojcik\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"headline\":\"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review\",\"datePublished\":\"2026-10-05T12:18:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/\"},\"wordCount\":2378,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png\",\"keywords\":[\"PHP\",\"Web Security\",\"WordPress\"],\"articleSection\":[\"WordPress-Plugins &amp; Tricks\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/\",\"name\":\"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review | Lukas Wojcik\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png\",\"datePublished\":\"2026-10-05T12:18:00+00:00\",\"description\":\"Sanitize early, escape late, always validate: fehlerhafter und korrigierter Code zu Eingaben, Ausgaben, Nonces, Capabilities und REST.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/#primaryimage\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png\",\"width\":1200,\"height\":630,\"caption\":\"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/de\\\/wordpress-plugins-tricks-de\\\/sanitizing-escaping-nonces-capabilities-plugin-review\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#website\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/\",\"name\":\"Lukas Wojcik - Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":[\"Person\",\"Organization\"],\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/#\\\/schema\\\/person\\\/895f7604f9b6b71aad9bba33af28d0f9\",\"name\":\"Lukas Wojcik\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"url\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"contentUrl\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\",\"width\":424,\"height\":636,\"caption\":\"Lukas Wojcik\"},\"logo\":{\"@id\":\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\\\/wp-content\\\/uploads\\\/2026\\\/07\\\/lw-x2.jpg\"},\"sameAs\":[\"https:\\\/\\\/www.lukaswojcik.com\\\/blog\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review | Lukas Wojcik","description":"Sanitize early, escape late, always validate: fehlerhafter und korrigierter Code zu Eingaben, Ausgaben, Nonces, Capabilities und REST.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/","og_locale":"en_US","og_type":"article","og_title":"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review | Lukas Wojcik","og_description":"Sanitize early, escape late, always validate: fehlerhafter und korrigierter Code zu Eingaben, Ausgaben, Nonces, Capabilities und REST.","og_url":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/","og_site_name":"Lukas Wojcik - Blog","article_published_time":"2026-10-05T12:18:00+00:00","og_image":[{"width":1200,"height":630,"url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png","type":"image\/png"}],"author":"Lukas Wojcik","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Lukas Wojcik","Est. reading time":"25 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/#article","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/"},"author":{"name":"Lukas Wojcik","@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"headline":"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review","datePublished":"2026-10-05T12:18:00+00:00","mainEntityOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/"},"wordCount":2378,"commentCount":0,"publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png","keywords":["PHP","Web Security","WordPress"],"articleSection":["WordPress-Plugins &amp; Tricks"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/","url":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/","name":"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review | Lukas Wojcik","isPartOf":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/#primaryimage"},"image":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/#primaryimage"},"thumbnailUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png","datePublished":"2026-10-05T12:18:00+00:00","description":"Sanitize early, escape late, always validate: fehlerhafter und korrigierter Code zu Eingaben, Ausgaben, Nonces, Capabilities und REST.","breadcrumb":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/#primaryimage","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/09\/fi-20858-sanitizing-escaping-nonces-capabilities-plugin-revie.png","width":1200,"height":630,"caption":"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review"},{"@type":"BreadcrumbList","@id":"https:\/\/www.lukaswojcik.com\/blog\/de\/wordpress-plugins-tricks-de\/sanitizing-escaping-nonces-capabilities-plugin-review\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.lukaswojcik.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Sanitizing, Escaping, Nonces, Capabilities im Plugin-Review"}]},{"@type":"WebSite","@id":"https:\/\/www.lukaswojcik.com\/blog\/#website","url":"https:\/\/www.lukaswojcik.com\/blog\/","name":"Lukas Wojcik - Blog","description":"","publisher":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.lukaswojcik.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":["Person","Organization"],"@id":"https:\/\/www.lukaswojcik.com\/blog\/#\/schema\/person\/895f7604f9b6b71aad9bba33af28d0f9","name":"Lukas Wojcik","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","url":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","contentUrl":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg","width":424,"height":636,"caption":"Lukas Wojcik"},"logo":{"@id":"https:\/\/www.lukaswojcik.com\/blog\/wp-content\/uploads\/2026\/07\/lw-x2.jpg"},"sameAs":["https:\/\/www.lukaswojcik.com\/blog"]}]}},"_links":{"self":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20858","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/comments?post=20858"}],"version-history":[{"count":1,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20858\/revisions"}],"predecessor-version":[{"id":21205,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/posts\/20858\/revisions\/21205"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media\/20895"}],"wp:attachment":[{"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/media?parent=20858"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/categories?post=20858"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.lukaswojcik.com\/blog\/wp-json\/wp\/v2\/tags?post=20858"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}