wp_kses() steckt hinter wp_kses_post(), der Kommentarbereinigung, Widget-Texten und jedem wp_kses( $html, $allowed ) in Ihrem Plugin. Im Trunk läuft es jetzt auf der WordPress HTML API statt auf regulären Ausdrücken. Laut dem Fortschrittsbericht von Dennis Snell soll die Änderung in WordPress 7.2 oder 7.3 landen, je nachdem, was die Tests ergeben. Dieser Beitrag zeigt, was sich im Output ändert, wie Sie Ihren betroffenen Code finden und wofür der befristete Opt-out gedacht ist.
Was ändert sich an wp_kses() genau
Die alte Implementierung zerlegte Markup mit regulären Ausdrücken. Die neue liest es mit dem Tokenizer der HTML API, entscheidet je Tag und Attribut, was erlaubt ist, und schreibt das Ergebnis neu. Der Pull Request heißt “KSES: Reimplement with Tag Processor” (PR 13271) und hängt am Trac-Ticket 66208. Laut PR liegt der neue Code in einer Funktion namens wp_sanitize_html_kses().
Die Absicht im Bericht ist eindeutig: wp_kses() soll nicht schlechter werden als vorher. Außerdem heißt es dort, Plugins und Themes sollten keine Änderungen brauchen. Beides kann stimmen, und Ihre Tests können trotzdem rot werden, denn “gleich sicher” heißt nicht “gleiche Bytes”. Die erneute Serialisierung ist der Kern der Änderung, und sie ändert Strings.
Quelle: Progress Report: wp_kses(), 7. Oktober 2026.
Stand am 15. Oktober 2026: Die Änderung ist in Trunk. Sie kam am 7. Oktober 2026 als Changeset 64233 (Commit auf GitHub), und laut Commit-Nachricht soll aufrufender Code keine Anpassung brauchen. Der Docblock von wp_kses() in Trunk trägt bereits @since 7.2.0, der Bericht spricht aber weiter von 7.2 oder 7.3. Was in die finale Version kommt, entscheidet die Testphase, nicht die Annotation. Am 9. Oktober folgten zwei weitere Commits in kses.php: Einer normalisiert alle Block-Delimiter, der andere serialisiert JSON-Attribute von Blöcken nur noch, wenn sie nicht leer sind (Dateihistorie).
Welche Eingaben liefern ein anderes Ergebnis
Der Bericht nennt Vorher-Nachher-Paare. Der Block unten ist daran angelehnt (mein Array erlaubter Tags, die Eingaben aus dem Bericht). Lassen Sie die Fälle auf Ihrem eigenen Trunk-Build laufen, bevor Sie meiner Abschrift trauen.
<?php
// Modelled on the examples in the Make Core progress report.
$allowed = array(
'a' => array( 'href' => true ),
'code' => true,
'img' => array( 'src' => true, 'class' => true ),
);
wp_kses( 'Click on my <script>alert(1)</script>!', $allowed );
// legacy: Click on my alert(1)!
// new: Click on my !
wp_kses( 'The <IMG class=bike class="boat" src=\'vehicle.png\' /> & the wheels…', $allowed );
// legacy: The <IMG class="bike" src="vehicle.png" /> & the wheels…
// new: The <img class="bike" src="vehicle.png"> & the wheels…
wp_kses( 'Add <code>/?preview=true§ion=grilling</code>.', $allowed );
// legacy: Add <code>/?preview=true&section=grilling</code>.
// new: Add <code>/?preview=true§ion=grilling</code>.
wp_kses( 'Click on the <butto', $allowed );
// legacy: Click on the <butto
// new: Click on the (trailing space, nothing else)Der Bericht gruppiert die Unterschiede so:
- Normalisierung. Tag-Namen werden kleingeschrieben, Attribute bekommen doppelte Anführungszeichen, bei doppelten Attributen gewinnt das erste, Zeichenreferenzen werden neu geschrieben.
- Lose spitze Klammern. Text wie
<$500oder>5yowurde früher verschluckt. Jetzt wird er zu<und>, der Text bleibt erhalten. - Mehrdeutiges kaufmännisches Und. Eine benannte Referenz ohne Semikolon, etwa
§ionin einer URL im Textknoten, wird so dekodiert wie im Browser. Deshalb ergibt der dritte Fall das Paragrafenzeichen. - Script und style. Sind diese Elemente nicht erlaubt, verschwindet ihr Inhalt samt Tags. Der alte Code ließ den Inhalt als sichtbaren Text stehen.
- Abgeschnittene Eingabe. Ein unvollständiges letztes Tag wird verworfen, nicht escaped.
- Spezielle Elemente. Tag-ähnlicher Text in einem erlaubten
<title>wird escaped, derselbe Text in einem<div>verschwindet mit dem unbekannten Tag. - CDATA, PLAINTEXT und unvollständige Spezialelemente. CDATA-Abschnitte und der Inhalt von
PLAINTEXTwerden zu normalem, escaptem Text. Ein<title>oder ein anderes Spezialelement, das unvollständig ankommt, wird komplett entfernt. - SVG und MathML. Inhalt, der aufwendiges Parsen braucht, lässt die Funktion abbrechen und nur das zurückgeben, was davor stand. Der Bericht zeigt ein
<li>mit MathML und verschachteltem<span>, das an dieser Stelle gekappt wird. - Harte Grenzen. Manche defekten Kommentare,
NOSCRIPTund verwechselbare Elemente werden jetzt abgelehnt. - Unausgeglichene Tags. Bleiben bewusst erhalten, weil bestehender Code darauf baut.
Typische Kandidaten in einem deutschsprachigen Shop: Produktbeschreibungen mit Maßangaben wie “<5 cm” aus einer Lieferantentabelle, Texte aus Rechtstext-Generatoren mit eingebettetem HTML und Cookie-Hinweise, die ein Inline-Skript ausgeben. Das ist eine Vermutung, wo Sie zuerst suchen sollten, keine Messung.
Ob <script> sich anders verhält, wenn Sie es im Array ausdrücklich erlauben, habe ich in den gelesenen Quellen nicht bestätigt gefunden. Prüfen Sie diesen Fall selbst, statt ein Ergebnis anzunehmen.
Warum wp_kses() den Tag Processor nutzt und nicht den HTML Processor
Die HTML API besteht aus zwei Teilen. WP_HTML_Tag_Processor gibt es seit WordPress 6.2. Er läuft Token für Token durch das Dokument: Tags, Kommentare und Textknoten (Klassenreferenz). WP_HTML_Processor kam mit 6.4, baut den Dokumentbaum auf und bricht bei Markup, das er noch nicht unterstützt, früh ab, um das Dokument nicht zu beschädigen (Klassenreferenz).
Laut einer Fußnote im Bericht setzt das neue wp_kses() auf den Tag Processor. Er parst jedes Tag, jeden Kommentar und jeden Textknoten, kennt aber die Baumstruktur des Dokuments nicht. Der HTML Processor baut diesen Baum, braucht dafür aber deutlich mehr Zeit und Aufwand. Die PR-Beschreibung ergänzt, warum offene Elemente nicht geschlossen werden: Viel Core-Code ruft wp_kses() absichtlich mit unvollständiger Eingabe auf, etwa mit einem öffnenden Wrapper-Tag samt Teil des Inhalts, getrennt vom schließenden Tag. Inhalte durch das Schließen solcher Elemente zu isolieren, würde Websites zerstören. Deshalb bleiben unausgeglichene Tags aus der Liste oben unausgeglichen.
Funktioniert mein eigenes Allowed-HTML-Array weiter
Meistens ja, denn die Erlaubnislogik bleibt gleich: Ein Tag oder Attribut steht im Array oder nicht. Geändert hat sich, was drumherum herauskommt. Drei Muster verdienen einen Blick.
- Arrays, die Inline-
scriptoderstyleerlauben. Ein Shortcode, der ein Inline-Skript ausgibt und durchwp_kses()mit erlaubtemscriptschickt, liegt genau in dem Bereich, den der Bericht berührt. Vergleichen Sie alte und neue Ausgabe. Prüfen Sie auch, ob das Skript besser überwp_add_inline_script()laufen sollte als durch den Sanitizer. - Arrays, die
svgodermatherlauben. Typisch sind Inline-Icon-Sets. Gilt die Regel aus dem Bericht, kann die Ausgabe am ersten Konstrukt abbrechen, das der Parser nicht verarbeitet. Vergleichen Sie jedes ausgelieferte Icon. - Arrays für
<title>und Ähnliches.<title>ist ein spezielles Element, sein Inhalt gilt als Text. Daher das Beispiel mit dem escapten<sneaky>.
Erlaubt Ihr Array nur a, strong, em, img und ein paar Attribute, bleibt der Effekt vermutlich kosmetisch: Aus <IMG ... /> wird <img ...>.
Wo WordPress wp_kses() aufruft, ohne dass Sie es tun
Core hängt den Sanitizer an das Speichern. kses_init() hängt in default-filters.php an den Hooks init und set_current_user und registriert die Filter nur, wenn dem aktuellen Benutzer die Berechtigung unfiltered_html fehlt (Referenz zu kses_init). Dann fügt kses_init_filters() wp_filter_kses an title_save_pre an, wp_filter_post_kses an content_save_pre, excerpt_save_pre und content_filtered_save_pre, und filtert pre_comment_content mit der einfachen Variante (Referenz zu kses_init_filters).
Die Referenzseite zu wp_kses() listet außerdem Dutzende direkte Aufrufe im Core, darunter Walker_Comment::filter_comment_text(), wp_filter_oembed_result(), img_caption_shortcode(), filter_block_kses_value() sowie die Installationsbildschirme für Plugins und Themes (Referenz zu wp_kses). Die Umstellung betrifft also Kommentare, oEmbed-Ergebnisse und Bildunterschriften, auch wenn Ihr Code wp_kses() nie selbst aufruft.
Für Tests hat das eine praktische Folge. Auf einer Einzelinstallation haben Administratoren und Redakteure unfiltered_html, in Multisite nur Super-Administratoren (Rollen und Berechtigungen). Ihre Inhalte laufen beim Speichern nicht durch kses. Testen Sie das Speichern mit einem Konto mit der Rolle Autor oder Mitarbeiter, denn dort greifen die Filter, und nicht nur mit dem eigenen Admin-Login.
Was geht in Tests kaputt, die bereinigtes HTML vergleichen
Alles, was einen exakten String prüft. Ein Test wie die erste Assertion unten besteht heute und kann nach der Umstellung scheitern, obwohl das Markup dasselbe bedeutet. Die zweite Assertion übersteht die Normalisierung.
<?php
// Brittle: pins the exact string the sanitizer happens to emit.
$this->assertSame( '<a href="/x">Read</a>', wp_kses_post( "<A HREF='/x'>Read</A>" ) );
// Sturdier: asserts on what the markup means.
$p = new WP_HTML_Tag_Processor( wp_kses_post( "<A HREF='/x'>Read</A>" ) );
$this->assertTrue( $p->next_tag( 'a' ) );
$this->assertSame( '/x', $p->get_attribute( 'href' ) );Prüfen Sie Verhalten, nicht Bytes. Fixtures, die stringgenau bleiben müssen, erzeugen Sie einmal auf Trunk neu, lesen den Diff als Mensch und committen den neuen Sollwert mit einem Kommentar, ab welcher Version er gilt. Ein pauschales “Snapshots aktualisieren” ist der Weg, auf dem eine echte Regression durchgewunken wird.
Visuelle Tests und Golden-Files brauchen dieselbe Sorgfalt. E-Mail-Templates, die Sie vor dem Versand bereinigen, sind ein typischer Ort, an dem ein gespeicherter String mit einem frischen verglichen wird.
So nutzen Sie assertEqualHTML() in Tests für wp_kses()
In den Kommentaren zum Bericht empfiehlt Jon Surrell assertEqualHTML() allen, deren Tests an kses-Unterschieden scheitern. Die Methode kam mit WordPress 6.9 als Teil von WP_UnitTestCase (Ticket 63527). Laut dem Beitrag von JuanMa Garrido im Developer Blog vom 26. Februar 2026 parst sie beide Strings mit WP_HTML_Processor und vergleicht die Bäume, nicht die Bytes.
Sie ignoriert genau die Art Unterschied, die die Umstellung mitbringt: Groß- und Kleinschreibung von Tag-Namen, Anführungszeichen, Reihenfolge von Attributen und Klassen, die verschiedenen Schreibweisen einer Zeichenreferenz und doppelte Attribute, bei denen das erste gewinnt. Sie schlägt weiterhin fehl, wenn sich Inhalt, ein Attributwert oder eine Klasse ändert oder ein Attribut fehlt oder hinzukommt.
<?php
$this->assertEqualHTML(
'<a href="/x">Read</a>',
wp_kses_post( "<A HREF='/x'>Read</A>" )
);Derselbe Beitrag zieht die Grenze: Wenn die exakte Serialisierung zählt, etwa beim Inhalt eines <script> oder bei einem bestimmten data-wp-context-Wert, bleiben Sie bei assertSame(). Für kses heißt das: assertSame() für jeden erlaubten Script- oder Style-Fall, assertEqualHTML() für den Rest. Läuft Ihre Testsuite gegen WordPress 6.9 oder neuer, steht die Methode bereits zur Verfügung.
Wie teste ich auf Trunk, ohne Produktion anzufassen
Drei Wege, alle auf Staging oder lokal. Das sind meine Vorschläge, kein Teil des offiziellen Berichts.
Plugin WordPress Beta Tester. Es stellt eine Staging-Seite auf Nightlies und später auf die 7.2-Betas um. Laut Zeitplan erscheint Beta 1 am 20. Oktober 2026. Die Änderung ist seit dem 7. Oktober in Trunk. Prüfen Sie trotzdem, ob der Filter wp_kses_force_legacy_parser im installierten Code existiert.
WP-CLI Nightly. Auf einer Wegwerfkopie:
# Staging only, never production.
wp core update --version=nightly --force
wp core version
wp eval-file bin/kses-diff.php > kses-diff.txt
tail -n 1 kses-diff.txtwp-env. Den Core auf den Trunk-Spiegel zeigen lassen und das Plugin einhängen:
{
"core": "WordPress/WordPress#master",
"plugins": [ "." ]
}Egal welcher Weg: Bestätigen Sie am Ende, dass der Filter existiert, etwa mit grep -rn wp_kses_force_legacy_parser wp-includes/. Fehlt er, testen Sie die Umstellung nicht.
Wie vergleiche ich die bereinigte Ausgabe echter Inhalte
Synthetische Fälle finden nur Fehler, die Sie sich schon ausgedacht haben. Der nützliche Test ist Ihre eigene Datenbank. Das Skript unten bereinigt jeden veröffentlichten Beitrag zweimal, einmal mit erzwungenem Legacy-Parser, einmal ohne, und gibt nur die abweichenden Beiträge aus.
<?php
// bin/kses-diff.php
// Run on a trunk build: wp eval-file bin/kses-diff.php > kses-diff.txt
$ids = get_posts( array(
'post_type' => 'any',
'post_status' => 'publish',
'numberposts' => -1,
'fields' => 'ids',
) );
$diff = 0;
foreach ( $ids as $id ) {
$raw = get_post_field( 'post_content', $id );
add_filter( 'wp_kses_force_legacy_parser', '__return_true' );
$old = wp_kses_post( $raw );
remove_filter( 'wp_kses_force_legacy_parser', '__return_true' );
$new = wp_kses_post( $raw );
if ( $old !== $new ) {
++$diff;
printf( "== %d %s\n--- old\n%s\n+++ new\n%s\n", $id, get_permalink( $id ), $old, $new );
}
}
printf( "%d of %d posts differ\n", $diff, count( $ids ) );Lassen Sie es auf einer Kopie der Produktionsdatenbank laufen, denn Redakteure fügen merkwürdige Dinge ein. Lesen Sie die ersten zwanzig Diffs von Hand. Rechnen Sie mit wenigen wiederkehrenden Ursachen (selbstschließende Tags, ein loses < in einer Maßangabe, Inline-SVG). Wer eine Ursache behebt, behebt viele Beiträge auf einmal.
Erweitern Sie das Netz nach demselben Muster: Ersetzen Sie wp_kses_post() durch den Aufruf, den Ihr Plugin mit eigenem Array macht, und füttern Sie ihn mit Optionswerten, Widget-Text und Kommentaren, nicht nur mit Beiträgen.
Bedenken Sie, was das Skript misst. Es ruft wp_kses_post() für alle Inhalte auf, auch für Beiträge von Benutzern mit unfiltered_html, die beim Speichern nie durch kses liefen. Bei diesen Beiträgen beschreibt der Diff keine heute sichtbare Änderung. Er zeigt, was passieren würde, wenn jemand ohne diese Berechtigung sie erneut speichert.
Wann nutze ich den Opt-out für den Legacy-Parser
Der Filter heißt wp_kses_force_legacy_parser. Gibt er true zurück, ist der Legacy-Parser wieder aktiv. Der Bericht beschreibt ihn als Möglichkeit für Seitenbetreiber, den Ersatzcode nicht auszuführen. Entscheidend ist das Wort befristet.
<?php
// wp-content/mu-plugins/kses-legacy-parser.php
// Stopgap only. Delete it once your output diff is clean.
add_filter( 'wp_kses_force_legacy_parser', '__return_true' );Zwei Situationen rechtfertigen ihn. Erstens: Sie haben im Testfenster eine Regression auf einer Kundenseite gefunden und müssen die Seite stabil halten, bis Code oder Bugreport erledigt sind. Zweitens: Ein Plugin, das Sie nicht kontrollieren, bricht, und der Hersteller hat keinen Fix. Legen Sie den Filter in ein MU-Plugin, damit er an einer Stelle sichtbar ist, und hängen Sie ein Ticket mit Löschdatum daran.
Liefern Sie ihn nicht als dauerhaften Standard aus. Die alte Implementierung hat bekannte Probleme, die die Umstellung beheben soll, laut PR-Beschreibung unter anderem PCRE-Abstürze bei großen Attributwerten und Skriptinhalt, der als sichtbarer Text erscheint. Ein stiller Opt-out behält diese Probleme und versteckt die Migration vor dem nächsten Entwickler.
Der Bericht verspricht keine lange Übergangszeit. Die ursprüngliche Implementierung bleibt demnach vorerst aus Kompatibilitätsgründen erhalten und verschwindet in künftigen Versionen, womöglich schon mit WordPress 7.2.0. Planen Sie so, als könnte der Legacy-Parser in der nächsten Hauptversion fehlen.
So funktioniert es in Trunk: wp_kses() wendet den Filter mit true als Standardwert an, und default-filters.php registriert darauf __return_false, was den neuen Code einschaltet. wp-settings.php lädt MU-Plugins nach default-filters.php, daher läuft Ihr __return_true mit Standardpriorität später und setzt sich durch (kses.php in Trunk). Rufen Sie wp_sanitize_html_kses() nicht direkt auf: Der Docblock kennzeichnet die Funktion als privat und als Übergangslösung.
Performance des neuen wp_kses() im Vergleich zum Legacy-Parser
Veröffentlichte Zahlen gibt es nicht. Auf die Frage nach Benchmarks in den Kommentaren zum Bericht antwortete Dennis Snell, dass diese Änderung keine enthält. Erwartet wird, dass der neue Code schneller ist und weniger Speicher braucht. Er nennt drei Gründe:
- Der alte Code zerlegt das Dokument in ein großes Token-Array und hält es im Speicher. Der neue Code geht Token für Token vorwärts, ohne dieses Array zu halten.
- Der alte Code macht mehrere Textverarbeitungsdurchläufe über das ganze Dokument. Der neue streicht die meisten davon und kommt größtenteils mit einem einzigen Durchlauf nach vorn aus.
- Der alte Code ruft immer
serialize_blocks( parse_blocks() )für das ganze Dokument auf. Der neue parst einen Block erst, wenn tatsächlich einer vorhanden ist, und verlässt sich für noch spätere Auswertung aufWP_Block_Parser.
Snell räumt auch ein, dass manche Teile der HTML-Verarbeitung in der neuen Implementierung aufwendiger sind. Wenn Ihre Website pro Anfrage viel Inhalt bereinigt, messen Sie auf Staging mit eingeschaltetem und ausgeschaltetem Filter und posten Sie die Zahlen unter dem Bericht. Genau darum bittet der Autor.
Wie sieht der Zeitplan für WordPress 7.2 aus
Aus dem Zeitplan-Beitrag zu 7.2, jeweils 15:00 UTC:
| Meilenstein | Datum |
|---|---|
| Beta 1 | 20. Oktober 2026 |
| Beta 2 | 27. Oktober 2026 |
| Beta 3 | 3. November 2026 |
| Beta 4 | 10. November 2026 |
| RC1 | 17. November 2026 |
| RC2 | 24. November 2026 |
| RC3 | 1. Dezember 2026 |
| Finale Version | 8. Dezember 2026 |
Der Bericht sagt 7.2 oder 7.3. Auf keine der beiden Versionen dürfen Sie fest zählen, 7.2 ausschließen können Sie aber auch nicht. Ein vernünftiger Plan: Diff-Skript auf Staging vor Beta 1, nochmal nach RC1, und alles Überraschende im Trac-Ticket melden, solange das Fenster offen ist. Genau dieses Feedback erbittet der Autor.
Was sollte eine Agentur diese Woche tun
- Den Code nach
wp_kses(,wp_kses_post(,wp_kses_data(und eigenen$allowed-Arrays durchsuchen. Auflisten, welche HTML von Nutzern oder Redakteuren bekommen. - Ein Trunk-Staging über einen der Wege oben aufsetzen und prüfen, dass der Filter existiert.
- Das Diff-Skript gegen eine Kopie der Produktionsinhalte laufen lassen. Nach Ursache sortieren, nicht nach Beitrag.
- Brüchige Tests auf Struktur umstellen, String-Fixtures einmal neu erzeugen und von einem Menschen prüfen lassen.
- Shortcodes und Blöcke prüfen, die Inline-Script, Style, SVG oder MathML ausgeben.
- Pro Kunde entscheiden, ob der Opt-out nötig ist, ihn dokumentieren und ein Löschdatum setzen.
Wenn ein Projekt Jahre eigener Bereinigungslogik rund um ein Plugin-Ökosystem trägt, passt so ein Audit in unsere Leistung individuelle WordPress-Entwicklung.
Quellen
- Progress Report: wp_kses(), Make Core, 7. Oktober 2026. Beispiele, Filtername, Aussage zu 7.2 oder 7.3.
- Zeitplan der 7.2 Release Party, Make Core, 6. Oktober 2026.
- Pull Request 13271, KSES: Reimplement with Tag Processor. Funktionsname, Filter, Liste der Verhaltensänderungen. Die Trac-Tickets 66208 und 65984 sind dort genannt.
- Trac-Ticket 66208. Trac zeigte mir eine Browserprüfung, die Angaben zum Ticket stammen daher aus dem PR und der Commit-Nachricht.
- Changeset 64233 und GitHub-Commit 7119e1b, 7. Oktober 2026. Datum in Trunk, Commit-Nachricht, geschlossene Tickets 37698, 58921, 59310 und 63208.
- kses.php in Trunk und default-filters.php. Standardwert des Filters, Annotation
@since 7.2.0, Hooks vonkses_init(). - Referenzen zu wp_kses(), kses_init(), kses_init_filters(), WP_HTML_Tag_Processor und WP_HTML_Processor auf developer.wordpress.org.
- A better way to test HTML in WordPress with assertEqualHTML(), WordPress Developer Blog, 26. Februar 2026.
- Roles and Capabilities, WordPress-Dokumentation. Welche Rollen
unfiltered_htmlhaben.
Zuletzt geprüft am 15. Oktober 2026 gegen Trunk. Das Verhalten von Trunk kann sich vor dem Release ändern.






