Wie finde ich eigentlich einen WordPress-Fehler?

WordPress ist über die Jahre hinweg ein riesiges Stück Software geworden. Da bleibt es nicht aus, dass auch viele Bugs darin enthalten sind. Aber nicht immer ist WordPress das Problem oder ein Plugin – manchmal ist es auch auch einfach eine bestimmte Kombination und eine falsche Erwartungshaltung. In diesem Artikel erzähle ich die Geschichte eines Tweets und meine Suche den Fehler zu finden bis zur Lösung. Wie immer habe ich dabei was gelernt und ihr könnt es auch, wenn ihr weiterlest …

Alles startete mit einem Tweet:

Frank „Löwenherz“ beschwert sich über einen Bug, der seit 6 Monaten keine Antwort bekommt. Soweit nicht ungewöhnlich.

Das Ganze soll mit dem Update auf 5.7.1 gekommen sein und betrifft die Beschreibung von Taxonomien, also auch Schlagwörter- oder Kategorien-Beschreibungen. Wird hier ein iFrame eingesetzt, so wird der Code beim Speichern gelöscht. Die bisherige Lösung klappte nicht mehr:

remove_filter( 'term_description', 'wp_kses_data' );

Mein erster Schritt war es zu schauen, was denn in 5.7.1 passiert ist. Dafür hat der WordPress-Trac einen eigenen Milestone-Filter. Aber in dem 5.7.1-Milestone gab es kein Ticket, was zu dem Problem passte. Auch die Beschreibung der Sicherheitslücken, die vielleicht nicht in Trac erfasst sind (Stichwort „responsible disclosure“), passte nicht zum Problem, da ging es um die Medienbibliothek und die REST-API.

Via Git Blame (total einfach auf Github) ließ sich auch leicht nachschauen, dass der Code, der die Filter einbaut genauso schon viel länger als WordPress 5.7.1 existiert.

// Kses only for textarea saves.
foreach ( array( 'pre_term_description', 'pre_link_description', 'pre_link_notes', 'pre_user_description' ) as $filter ) {
	add_filter( $filter, 'wp_filter_kses' );
}

// Kses only for textarea admin displays.
if ( is_admin() ) {
	foreach ( array( 'term_description', 'link_description', 'link_notes', 'user_description' ) as $filter ) {
		add_filter( $filter, 'wp_kses_data' );
	}
	add_filter( 'comment_text', 'wp_kses_post' );
}

Es gibt zwei Filterorte: pre_term_description und term_description. Beide bekommen einen passenden kses zum Filtern von unerwünschten Code. Wobei schon auffällt, dass der obige angeblich funktionierende Code eigentlich nie klappen konnte, denn der pre_term_description-Filter löscht den iFrame-Tag bereits.

Das passte also einfach alles nicht zusammen. Warum sollte das ein Bug sein?

Nach einer Nachfrage auf Twitter wurde das Bild klarer, denn nun kam eine neue Information dazu:

Dashboard > Schlagwörter > Beschreibung (dank YoastSEO mit visuellem Editor). Wenn ich dort ein yT-Video integrieren möchte, ging dies nur via iFrames > wird seit 5.7.1 beim Speichern entfernt.

Aha! Yoast war also beteiligt. Damit gab es einen weiteren Kandidaten, den es zu berücksichtigen gilt. Praktischerweise ist auch Yoast auf Github und ich kann schnell suchen. Da war es auch schon:

/**
	 * Allows post-kses-filtered HTML in term descriptions.
	 */
	public function custom_category_descriptions_allow_html() {
		remove_filter( 'term_description', 'wp_kses_data' );
		remove_filter( 'pre_term_description', 'wp_filter_kses' );
		add_filter( 'term_description', 'wp_kses_post' );
		add_filter( 'pre_term_description', 'wp_filter_post_kses' );
	}

Yoast ergänzt die Textarea in den Schlagwörter- oder Kategorien-Beschreibungen mit einem visuellen Editor, damit einfacher formatiert werden kann. Dadurch werden allerdings andere kses-Versionen notwendig. In einer Textarea, die nur reinen Text ausgeben soll, ist weniger erlaubt, als in einer Textarea mit visuellem Editor. Daher wird der WP-Standardfilter entfernt und mit einem nun passenderen „Post“-Kontext-Filter ersetzt.

Ein kleiner Testlauf mit GeneratePress (das stellt die Beschreibungen standardmäßig dar) und Yoast ist erfolgreich:

function remove_kses() {
	remove_filter( 'term_description', 'wp_kses_post' );
	remove_filter( 'pre_term_description', 'wp_filter_post_kses' );
}
add_action( 'init', 'remove_kses', 11 );

Dieser Code entfernt nun den Filter von Yoast und ermöglicht nun wunschgemäß das Setzen eines iFrames in der Beschreibung. Aber das ist nur ein „Proof of Concept“! Denn das vollständige Entfernen des Filters öffnet ein Tor, dass sich im unglücklichsten Falle als Vektor für einen Angriff eignen könnte. Wir sollten daher lieber nicht auf diesen wichtigen Filter verzichten. Daher lohnt ein Blick, um was es sich bei kses eigentlich handelt und wie es verändert werden kann.

KSES ist ein rekursives Akronym und steht für “KSES Strips Evil Scripts”, es entfernt alles, was nicht explizit erlaubt ist. Das funktioniert so: KSES hat drei Parameter. Nummer eins ist der zu filternde Text und Nummer 3 sind die erlaubten Protokolle. Nummer 2 ist das Spannende, hier wird entweder direkt ein Array mit den erlaubten Tags übergeben oder ein String mit dem Kontext in dem der Filter wirken soll.

Im Originalzustand wird ja wp_kses_data an term_description gehängt und wp_filter_kses an pre_term_description. Wo bei wp_kses_data nur wp_kses aufruft und als Kontext den aktuellen Filter mit übergibt, was zum Defaultwert führt, also nur den globalem Wert von $allowedtags (siehe wp_kses_allowed_html) und der erlaubt keine iFrames.

Neben der Funktion wp_kses_allowed_html gibt es auch einen gleichnamigen Filter, über den wir den Kontext abfragen und gegebenenfalls die erlaubten Tags filtern können.

Anstatt also den Schutzfilter komplett zu entfernen, erweitern wir die erlaubten Tags einfach nur um das iFrame-Tag und seine Attribute:

/**
 * Add iFrame to allowed wp_kses tags
 *
 * @param array  $tags Allowed tags, attributes, and/or entities.
 * @param string $context Context to judge allowed tags by.
 *
 * @return array
 */
function custom_wpkses_post_tags( $tags, $context ) {

	if ( 'pre_term_description' === $context ) {
		$tags['iframe'] = array(
			'src'             => true,
			'height'          => true,
			'width'           => true,
			'frameborder'     => true,
			'title'           => true,
			'allow'           => true,
			'allowfullscreen' => true,
		);
	}

	return $tags;
}

add_filter( 'wp_kses_allowed_html', 'custom_wpkses_post_tags', 10, 2 );

Quelle: Für YouTube erweiterte Version von https://gist.github.com/bjorn2404/8afe35383a29d2dd1135ae0a39dc018c

(Kurze Anmerkung: Für eine vollständige Unterstützung gäbe es noch mehr globale und lokale Attribute einzurichten.)

Für den Fall mit aktiviertem Yoast gilt ja der Post-Kontext und somit nicht mehr pre_term_description, sondern eben post für die Prüfung von $context.

Tadaa! Jetzt wird kein iFrame-Tag mehr aus der Beschreibung gelöscht. Problem gelöst und eine Menge über kses auf dem Weg gelernt. Es verhält sich unterschiedlich je nach Kontext und kann/muss auch je nach Kontext unterschiedlich gefiltert werden!

Damit ist das Trac-Ticket gar kein Bug, sondern nur ein Verständnisproblem. Bei so einem komplexen und verschachtelten System wie kses sicher nicht ungewöhnlich.

Hattest du auch schon mal mit kses zu tun? Hast du spannende Lösungen damit gebaut? Oder ähnliche Probleme damit gehabt? Dann freue ich mich über deine Geschichte in den Kommentaren!

10 Antworten auf Wie finde ich eigentlich einen WordPress-Fehler?

  1. kses ist schon eine mächtige Funktion und zeigt mal wieder, dass man im Grundsatz WordPress das Handling von Strings überlassen sollte.
    Sehr schön, wieder mit der Nase darauf gestupst worden zu sein.

    Häufig ist es nur so, dass solche Funktionen unerwünschte Nebeneffekte haben. Eine Fehlfunktion ist es ja so gesehen nicht: wp_kses tut das, was es soll.
    Ich Frage mich nur, wie man so etwas zukünftig sichtbarer machen könnte. Zwar hast du nach dem richtigen Hinweis recht schnell den Ansatz gefunden – doch kann dies der Laie auch?

    Natürlich nicht. Aber wie könnte er es?
    Jeden Codechange ins Changelog schreiben bläht eben dieses auf – und kann vom Laien auch nicht verstanden werden.
    Also braucht es vermutlich weiterhin so pfiffige Leute wie dich, die ein Verständnis von Code, Git und Filtern hat.

    Danke, dass du uns auf die Reise mitgenommen hast.

    • Ich denke auch, dass dies schwierig für den Laien ist. Und nicht nur für den. Wer nicht startet und sich konkret durch den Code wühlt, der kommt auch nicht dahinter. Daher auch der Titel des Artikels. Ich wollte zeigen, wie ich gedanklich vorgegangen bin und zu was mich welche Erkenntnis dann gebracht hat. Es wirkt jetzt vielleicht total klar im Artikel, aber am Anfang hatte ich überhaupt keine Ahnung wohin mich die Recherche bringt. Es war nicht so, dass mir sofort klar war, woran es lag. Ich habe Bereiche durchsucht und nichts gefunden, nachgefragt und dann weiter gesucht. Jetzt habe ich nicht nur das Rätsel gelöst, sondern kenne auch den Filter und wie man ihn einsetzt und genau das ist das großartige am WordPress-Support: Spannende Fragen bringen einen immer auch selbst weiter.

      Insofern freue ich mich besonders über deine letzten beiden Sätze und antworte: Schön, dass DU mitgekommen bist 😉

  2. Vielen Dank für deine Hilfe Torsten. In diese Tiefen hab ich mich nie begeben, sondern schaue in erster Linie, was notwendig ist und wie ich es zum Laufen bringe 🙂
    Hab deinen Code integriert, iFrame bei einem Schlagwort eingefügt (geht natürlich nur im Textmodus), gespeichert und weg war der iFrame wieder. Also keine Änderung feststellbar.
    Wobei iFrames mir nur als Notbehelf dienen, cooler wäre es, wenn ich Videos wie bei Post einfach über das Einfügen einer YouTube-URL integrieren könnte. Interessanterweise wird solch ein Video auch im visuellen Editor angezeigt, die https-Zeile beim Speichern auch nicht entfernt – nur in der Live-Ansicht erfolgt die Umwandlung dann nicht.

    • Gerne! 🙂

      Welchen Code genau hast du denn wo eingebaut? Ist Yoast aktiv? (Wenn Yoast aktiv ist, musst im letzten Code-Beispiel den $context-Check auf „post“ ändern, sonst klappt es nicht.)

      Welches Theme nutzt du? Oder falls du die Ausgabe selbst gecodet hast, welchen Code hast dafür genau genutzt?

      Ich habe sämtlichen Code getestet und das funktioniert auf jeden Fall in der Kombi wie im Artikel angegeben. Wenn es bei dir nicht klappt, dann schießt da noch ein anderes Theme/Plugin quer.

      Das Autoembedding lässt sich leicht über den gleichen Filter hinzufügen:
      add_filter( 'term_description', array( $wp_embed, 'autoembed'), 8 );

      Dann musst du nur noch den YouTube-Link einfügen. Weiterer Vorteil: Tools wie Embed Privacy greifen dann auch.

      Wenn der iframe-Code beim Speichern nicht gelöscht wird, aber bei der Ausgabe nicht ausgegeben wird, dann könnte bei der Ausgabe z.B. durch das Theme noch etwas gefiltert werden. Daher meine Frage nach dem Code oben.

      • Hi,

        „welchen Code genau hast du denn wo eingebaut?“
        Zeile 1-26 wie von dir oben zuletzt gepostet 1:1. Zuzüglich deinem letzten Tipp mit
        add_filter( ‚term_description‘, array( $wp_embed, ‚autoembed‘), 8 );
        Den alten diesbezüglichen Code nur noch als Kommentar drin.
        functions.php

        „Ist Yoast aktiv?“
        Klar.

        „(Wenn Yoast aktiv ist, musst im letzten Code-Beispiel den $context-Check auf „post“ ändern, sonst klappt es nicht.)“
        Öhm, ich seh zwar $context, aber was ich konkret auf post ändern muss (alles mit $tags?), dafür fehlt mir dann doch zu sehr Codingverständnis. Vielleicht liegt hier noch der Fehler.

        „Welches Theme nutzt du?“
        Hab bei diesen Projekten nur Elmathemes im Einsatz, in dem Fall Yoko.

        Wie gesagt, auf die iFrame-Lösung kann ich verzichten, wenn die Direkteinbindung von yT klappt.

        • Ändere bitte Zeile 11 in diese Zeile (das ist das, was ich gemeint habe):
          if ( 'post' === $context ) {

          Da Yoast den Kontext dahingehend ändert ist es mit Yoast so herum zu prüfen. Dann sollte es klappen.

          In Yoko sehe ich die Beschreibung in der category.php und dort ist zwar ein Filter archive_meta, aber der hat standardmäßig keine Nutzung. Das sollte also eigentlich nichts kaputt machen.

          Freue mich über Feedback, wenn es jetzt klappt!

          • Danke, hab es entsprechend geändert. Cache natürlich auch gelöscht etc.
            Im visuellen Editor werden sowohl iFrame als auch Videolink umgewandelt und als Videos angezeigt, nach dem Speichern ist der iFrame wieder gelöscht. Videolink selbst wird einfach nur als Text angezeigt.

            Hm, Problem ist wohl doch komplexer.

            Hab nur wenige Plugins drin. Die beiden Einzigen, die Auswirkungen haben dürften, sind Classic Editor und Embed videos and respect privacy.

            Am Yoko-Theme hab ich – wie bei allen Elmathemes – ein paar Veränderungen in Sachen SEO vorgenommen, u.a in category.php und tag.php. Dies betrifft aber in der Regel nur die vorher suboptimale Überschriften-Struktur und die Anzeige der tag_description() – also an sich nix gravierendes. Kann natürlich vorkommen, dass ich mal vergesse, ein Child analog zum Main-Theme zu aktualisieren, aber bei Yoko ist ja schon lange nichts mehr passiert.

          • Embed videos and respect privacy ist seit 4 Jahren nicht mehr aktualisiert worden und nur bis WP 4.9 kompatibel. Wechsel lieber zu Embed Privacy von Epiphyt. Das macht im Prinzip das Gleiche, denke ich.

            Habe meinen Code gerade nochmal mit Yoko getestet. Funktioniert ohne Probleme.

            Versuche doch mal das Plugin zu deaktivieren. Geht es dann? Über das Health Check Plugin kannst du ja auch einen Troubleshooting-Modus starten und so die Plugins einzelen aktivieren und so testen, welches Plugin querschießt.

  3. Danke für deine Tipps Torsten.

    Hab gestern „Embed videos and respect privacy“ entfernt, keine Auswirkungen.
    Das Projekt lief früher auf wpSEO, das ich Anfang des Jahres auf YoastSEO umgestellt hatte. Kurz darauf stellte ich fest, dass das Einbetten von Videos bei Schlagworten nicht mehr ging. Ich dachte, es wär 5.7.1 (zumal dort etwas zu finden war zum Thema Security), aber scheinbar lag es wie von dir eruiert an Yoast (wobei einige Projekte schon länger mit Yoast laufen und mir dort nie etwas aufgefallen war).

    Hab mir zum Vergleich ein anderes Projekt genommen, das auf dem Baylys-Theme basiert. Den alten Code entfernt und 1:1 durch deinen Neuen ersetzt. Und siehe da: Es klappt. Dort sind außer „Add From Server“ dieselben Plugins (und einige mehr) installiert, hab das genannte Plugin bei Projekt1 deaktiviert, keine Auswirkungen. Auch die functions.php hat bei Projekt2 nun allen Code, den auch Projekt1 hat, ohne Auswirkungen, Video funzt weiter.

    Da ich den Code eh brauche, ändere ich das erstmal bei weiteren Projekten ab und schau, was mir dabei einfällt. An den Plugins oder der functions scheint es schon mal nicht zu liegen.

    • Eben bei einem anderen Yoko-Projekt getestet, klappt alles. Also nochmal bei Projekt1 geschaut. Festgestellt, dass ich die Änderungen beim falschen Child-Theme getestet hab (derzeit liegen zwei auf dem Server, weil ich eine Weile ein anderes Theme als das von Elmastudio ausprobiert hatte). Änderungen nun in der korrekten fuctions.php – alles läuft, hurra.

      Fazit: Das ursprüngliche Problem war YoastSEO und nicht WP (qed). Und das letzte Problem sitzt mal wieder vor dem Monitor 🙂

      Bleibt zu hoffen, dass Yoast nicht wieder was ändert, das diese Lösung mit einem Schlag aushebeln würde…

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.