WordPress 6.7 Plugin-Sicherheit — OWASP-Top-10-Praxis
WordPress ist 2025 das CMS mit den meisten CVEs (~4.300 Einträge im Ecosystem). Wer Plugins entwickelt, sollte die OWASP Top 10 nicht abstrakt, sondern als konkrete WordPress-Patterns kennen.
WordPress 6.7 (Stand Mitte 2026) treibt nach offiziellen W3Techs-Zahlen rund 43 Prozent aller Websites weltweit. Mit dieser Reichweite kommt eine entsprechende Angriffsfläche: Patchstack und Wordfence haben für 2025 zusammen etwa 4.300 CVEs im WordPress-Ecosystem dokumentiert, fast 95 Prozent davon in Plugins, nicht im Core. Wer ein Plugin schreibt, schreibt potenziell Code, der auf 100.000 Sites läuft — und jeder Fehler skaliert mit.
Die OWASP Top 10 sind das Standard-Framework für Web-Sicherheit. Dieser Artikel übersetzt sie in konkrete WordPress-Plugin-Patterns.
A01: Broken Access Control
Die häufigste Klasse echter Plugin-Vulns. Eine Plugin-Funktion, die im Admin-Backend aufgerufen wird, aber nicht prüft, ob der aktuelle User die nötige Capability hat.
// FALSCH
add_action('admin_post_delete_item', function() {
$id = (int) $_POST['id'];
wp_delete_post($id, true);
});
// RICHTIG
add_action('admin_post_delete_item', function() {
if (!current_user_can('delete_posts')) {
wp_die('Insufficient permissions', 403);
}
check_admin_referer('delete_item_nonce');
$id = (int) $_POST['id'];
wp_delete_post($id, true);
});
Zwei Checks, die in jeder schreibenden Plugin-Funktion stehen müssen: current_user_can() für die Capability und check_admin_referer() (oder check_ajax_referer() für AJAX) gegen CSRF. Capabilities sind feiner als Rollen — eine schreibende Funktion sollte die spezifische Capability prüfen, nicht pauschal manage_options.
REST-API-Endpoints brauchen ihren eigenen permission_callback:
register_rest_route('myplugin/v1', '/items/(?P<id>\d+)', [
'methods' => 'DELETE',
'callback' => 'myplugin_delete_item',
'permission_callback' => fn() => current_user_can('delete_posts'),
]);
Ein permission_callback der '__return_true' zurückgibt, ist ein offener Endpoint. Das ist Mitte 2026 immer noch die zweithäufigste Quelle von WordPress-CVEs.
A02: Cryptographic Failures
WordPress-Core verwendet seit 6.4 password_hash() mit PASSWORD_BCRYPT als Default. Plugin-Code, der eigene User-Tabellen pflegt (Membership-Plugins, B2B-Portale), sollte niemals md5/sha1 für Passwörter verwenden.
// FALSCH
$hash = md5($password);
// RICHTIG
$hash = wp_hash_password($password);
// oder, wenn explizit Core-unabhängig:
$hash = password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
Für Verifikation:
if (wp_check_password($password, $stored_hash, $user_id)) { ... }
Sensible Daten in der Datenbank (API-Keys, OAuth-Refresh-Tokens) gehören in wp_options mit autoload = 'no' und verschlüsselt mit openssl_encrypt (AES-256-GCM) unter einem in wp-config.php gespeicherten Schlüssel — nicht im Klartext.
A03: Injection
SQL-Injection ist die archetypische WordPress-Vuln. Der Core liefert mit $wpdb->prepare() das Werkzeug — Plugin-Code, das es nicht nutzt, ist unentschuldbar.
global $wpdb;
// FALSCH
$results = $wpdb->get_results(
"SELECT * FROM {$wpdb->prefix}items WHERE status = '{$_GET['status']}'"
);
// RICHTIG
$results = $wpdb->get_results(
$wpdb->prepare(
"SELECT * FROM {$wpdb->prefix}items WHERE status = %s",
$_GET['status']
)
);
Die Placeholder sind streng typisiert: %s für String, %d für Integer, %f für Float. Seit WordPress 6.2 wird %i für Identifier (Tabellennamen, Spaltennamen) unterstützt — vorher mussten Identifier mit Allow-Lists validiert werden.
Für XSS gilt: jede Ausgabe escapen. Die Faustregel ist „escape late, escape close to output”:
echo esc_html($user_input);
echo esc_attr($attr_value);
echo esc_url($url);
echo wp_kses_post($html_with_some_tags_allowed);
wp_kses_post() erlaubt die HTML-Tags, die auch im Post-Editor erlaubt sind — sicherer Mittelweg zwischen „kompletter Strip” und „raw HTML”.
A05: Security Misconfiguration
WordPress-Core setzt einige Sicherheits-Header (X-XSS-Protection, X-Content-Type-Options), aber CSP und X-Frame-Options nicht. Plugins, die ein Admin-UI rendern, sollten Sicherheits-Header via send_headers-Hook ergänzen:
add_action('send_headers', function() {
if (is_admin()) {
header('X-Frame-Options: DENY');
header(
"Content-Security-Policy: default-src 'self'; " .
"script-src 'self' 'unsafe-inline'; " .
"img-src 'self' data: https:; " .
"connect-src 'self';"
);
header('Referrer-Policy: strict-origin-when-cross-origin');
}
});
CSP mit 'unsafe-inline' ist nicht ideal — aber die WordPress-Admin-UI selbst nutzt inline-Scripts an vielen Stellen, ein striktes CSP würde das Backend kaputt machen. Strikte CSP ist nur auf isolierten Plugin-Settings-Pages realistisch, die du selbst kontrollierst.
A06: Vulnerable and Outdated Components
Plugins, die composer.json für vendor-Dependencies nutzen, sollten regelmäßig composer audit laufen lassen:
composer audit --format=json > audit.json
In CI als Quality-Gate verankern — ein neuer CVE in einer Composer-Dependency bricht den Build. Für JavaScript-Build-Tools (npm audit, yarn npm audit) analog.
WordPress selbst pflegt mit wp_get_plugin_dependencies() (seit 6.5) eine Mechanik, um Inter-Plugin-Dependencies zu deklarieren. Wer auf Patchstack abonniert ist, bekommt Plugin-CVE-Feeds automatisiert.
A07: Identification and Authentication Failures
WordPress-Core liefert keine 2FA out of the box. Plugin-Entwickler, deren Plugin Admin-Funktionen exponiert, sollten mit etablierten 2FA-Plugins (Two-Factor von dem Core-Contributor-Team, WP 2FA von Melapress) zusammenarbeiten — nicht eigene 2FA-Implementierungen schreiben.
Rate-Limiting für Login-Endpoints ist ebenfalls nicht Core. Für plugin-eigene Login-Flows (REST-Endpoint für eine Mobile-App):
add_action('rest_api_init', function() {
register_rest_route('myplugin/v1', '/login', [
'methods' => 'POST',
'callback' => 'myplugin_rest_login',
'permission_callback' => function() {
$ip = $_SERVER['REMOTE_ADDR'];
$key = "myplugin_login_attempts_{$ip}";
$attempts = (int) get_transient($key);
if ($attempts >= 5) return false;
set_transient($key, $attempts + 1, MINUTE_IN_SECONDS * 15);
return true;
},
]);
});
Fünf Versuche pro fünfzehn Minuten, IP-basiert. Im Produktiv-Setup hinter Cloudflare lieber CF-Connecting-IP auswerten, sonst sieht das Plugin nur die Cloudflare-Edge-IP.
A08: Software and Data Integrity Failures
WordPress-Plugin-Updates aus dem offiziellen Repository sind nicht digital signiert — der Update-Server liefert das ZIP, der Core verifiziert den SHA1-Hash gegen die Repository-Metadaten, mehr nicht. Premium-Plugins mit eigenem Update-Server sollten zumindest ein signiertes Manifest ausliefern:
add_filter('upgrader_pre_download', function($reply, $package, $upgrader) {
if (str_starts_with($package, 'https://updates.myplugin.com/')) {
$sig_url = $package . '.sig';
$sig = wp_remote_get($sig_url);
if (!verify_signature($sig['body'], MYPLUGIN_PUBLIC_KEY, $package)) {
return new WP_Error('bad_signature', 'Update signature invalid');
}
}
return $reply;
}, 10, 3);
Für die Signatur-Verifikation sodium_crypto_sign_verify_detached() aus libsodium (in PHP seit 7.2 verfügbar).
A09: Security Logging and Monitoring Failures
WordPress hat keinen Audit-Log out of the box. Für sicherheitskritische Aktionen im eigenen Plugin solltest du wenigstens ein einfaches Logging via error_log() oder eine dedizierte Tabelle führen:
function myplugin_audit_log(string $action, array $context = []): void {
$entry = [
'time' => gmdate('c'),
'user' => get_current_user_id(),
'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
'action' => $action,
'context' => $context,
];
error_log('MYPLUGIN_AUDIT: ' . wp_json_encode($entry));
}
WP-CLI hat seit 2.10 einen wp doctor check-Befehl, der Security-Posture-Checks fährt — in CI nach jedem Deployment einbauen.
A10: Server-Side Request Forgery
Plugins, die externe URLs abrufen (Webhook-Subscriber, Image-Importer), öffnen SSRF-Vektoren. wp_remote_get() selbst hat keinen URL-Filter — der muss manuell stehen.
function myplugin_safe_remote_get(string $url) {
$allowed_hosts = ['api.partner.com', 'cdn.partner.com'];
$host = wp_parse_url($url, PHP_URL_HOST);
if (!in_array($host, $allowed_hosts, true)) {
return new WP_Error('blocked_host', 'Host not on allow-list');
}
$ip = gethostbyname($host);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
return new WP_Error('private_ip', 'Resolved to private IP');
}
return wp_remote_get($url, ['timeout' => 5, 'redirection' => 0]);
}
Wichtig sind drei Schichten: Host-Allow-List, DNS-Rebinding-Schutz (IP-Range-Check), und redirection = 0, damit der externe Server keinen 302 auf http://169.254.169.254/ schicken kann.
Pen-Test-Tools und Security-Plugins
Für ein finales Audit vor Release:
- WPScan. CLI-Tool und Online-DB für bekannte WordPress-/Plugin-/Theme-Vulns.
wpscan --url https://staging.example.com --enumerate vp,vt,u. - Sucuri SiteCheck. Web-basierter Scanner, findet Defacements und Blacklist-Einträge.
- Wordfence Security. Plugin mit Web-Application-Firewall, Brute-Force-Schutz, Malware-Scan. Free-Version reicht für die meisten Sites.
- Sucuri Security. Konkurrenz-Plugin mit Activity-Audit-Log.
- iThemes Security (jetzt Solid Security). File-Change-Detection, 2FA, Brute-Force-Schutz.
Fazit
OWASP Top 10 ist kein abstraktes Framework — für WordPress-Plugins lässt sich jeder Punkt in zwei bis fünf konkrete Code-Patterns übersetzen. Wer diese Patterns in seinen Plugin-Boilerplate übernimmt (PHPStan WordPress-Erweiterung + WordPress-Coding-Standards + die hier gezeigten Capability-/Nonce-/Prepare-Patterns), eliminiert die häufigsten 80 Prozent der realistischen Vulns. Die letzten 20 Prozent (Logikfehler, Race Conditions, Crypto-Edge-Cases) bleiben — dafür existiert das externe Audit.