wp_kses() sobre la HTML API: qué probar antes de WordPress 7.2

wp_kses() sobre la HTML API: qué probar antes de WordPress 7.2

Última verificación: 15 de octubre de 2026
17 min de lectura
Tutorial
500+ proyectos WP
Desarrollador full-stack

wp_kses() está detrás de wp_kses_post(), de la limpieza de comentarios, del texto de widgets y de cada llamada wp_kses( $html, $allowed ) de tu plugin. En trunk ahora funciona sobre la HTML API de WordPress en lugar de expresiones regulares. Según el informe de progreso de Dennis Snell, el cambio debería llegar a WordPress 7.2 o 7.3, según lo que descubran las pruebas. Este artículo explica qué cambia en la salida, cómo localizar el código tuyo que se ve afectado y para qué sirve la salida de emergencia temporal.

#Qué cambia exactamente en wp_kses()

La implementación antigua desmontaba el marcado con expresiones regulares. La nueva lo lee con el tokenizador de la HTML API, decide por etiqueta y atributo qué se permite y vuelve a escribir el resultado. El pull request se titula “KSES: Reimplement with Tag Processor” (PR 13271) y está vinculado al ticket de Trac 66208. Según el PR, el código nuevo vive en una función llamada wp_sanitize_html_kses().

La intención del informe es clara: wp_kses() no debe quedar peor de lo que estaba. El informe dice además que plugins y temas no deberían necesitar cambios. Ambas frases pueden ser ciertas y tus pruebas ponerse rojas igualmente, porque “igual de seguro” no significa “los mismos bytes”. La nueva serialización es el centro del cambio, y cambia cadenas.

Fuente: Progress Report: wp_kses(), 7 de octubre de 2026.

Estado a 15 de octubre de 2026: el cambio ya está en trunk. Entró el 7 de octubre de 2026 como changeset 64233 (commit en GitHub), y el mensaje del commit dice que el código que llama a la función no debería necesitar cambios. El docblock de wp_kses() en trunk ya lleva la anotación @since 7.2.0, pero el informe sigue hablando de 7.2 o 7.3, y lo que llegue a la versión final lo decide el periodo de pruebas, no la anotación. El 9 de octubre entraron dos commits de seguimiento en kses.php: uno normaliza todos los delimitadores de bloque y otro solo serializa los atributos JSON de bloque que no están vacíos (historial del archivo).

#Qué entradas producen una salida distinta

El informe muestra pares de antes y después. El bloque de abajo se basa en ellos (mi array de etiquetas permitidas, las entradas del informe). Ejecuta los casos en tu propio build de trunk antes de fiarte de mi transcripción.

<?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="bo&#x0061;t" src=\'vehicle.png\' /> & the wheels&hellip;', $allowed );
// legacy: The <IMG class="bike" src="vehicle.png" /> &amp; the wheels&hellip;
// new:    The <img class="bike" src="vehicle.png"> &amp; the wheels…

wp_kses( 'Add <code>/?preview=true&section=grilling</code>.', $allowed );
// legacy: Add <code>/?preview=true&amp;section=grilling</code>.
// new:    Add <code>/?preview=true§ion=grilling</code>.

wp_kses( 'Click on the <butto', $allowed );
// legacy: Click on the &lt;butto
// new:    Click on the (trailing space, nothing else)

El informe agrupa las diferencias así:

  • Normalización. Los nombres de etiqueta pasan a minúsculas, los atributos llevan comillas dobles, si un atributo se repite gana el primero y las referencias de caracteres se reescriben.
  • Corchetes angulares sueltos. Texto como <$500 o >5yo se tragaba. Ahora se convierte en &lt; y &gt;, y el texto sobrevive.
  • Ampersand ambiguo. Una referencia con nombre sin punto y coma, como &section en una URL dentro de un nodo de texto, se decodifica como lo hace el navegador. Por eso el tercer caso produce el signo de párrafo.
  • Script y style. Cuando estos elementos no están permitidos, su contenido desaparece junto con las etiquetas. El código antiguo dejaba el contenido como texto visible.
  • Entrada truncada. Una última etiqueta incompleta se descarta, no se escapa.
  • Elementos especiales. El texto con forma de etiqueta dentro de un <title> permitido se escapa, mientras que el mismo texto dentro de un <div> desaparece con la etiqueta desconocida.
  • CDATA, PLAINTEXT y elementos especiales incompletos. Las secciones CDATA y el contenido de PLAINTEXT se convierten en texto escapado normal. Un <title> u otro elemento especial que llega incompleto se elimina entero.
  • SVG y MathML. El contenido que exige un análisis complejo hace que la función se detenga y devuelva solo lo anterior a ese punto. El informe muestra un <li> con MathML y un <span> anidado cortado ahí.
  • Límites duros. Algunos comentarios mal formados, NOSCRIPT y elementos confundibles se rechazan ahora.
  • Etiquetas desbalanceadas. Se conservan a propósito, porque el código existente depende de ello.

En una tienda WooCommerce típica, los primeros sitios donde mirar son descripciones de producto con medidas como “<5 cm” pegadas desde la hoja de cálculo del proveedor, HTML importado de proveedores con etiquetas en mayúsculas y constructores de páginas que imprimen SVG en línea. Es una suposición sobre dónde buscar, no una medición.

En las fuentes que leí no encontré confirmado cómo se comporta <script> cuando lo permites de forma expresa en el array. Prueba ese caso tú mismo en lugar de dar por hecho un resultado.

#Por qué wp_kses() usa el Tag Processor y no el HTML Processor

La HTML API tiene dos piezas. WP_HTML_Tag_Processor existe desde WordPress 6.2 y recorre el documento token a token: etiquetas, comentarios y nodos de texto (referencia de la clase). WP_HTML_Processor llegó en 6.4, construye el árbol del documento y, cuando encuentra marcado que todavía no soporta, aborta pronto para no romper el documento (referencia de la clase).

Según una nota al pie del informe, el nuevo wp_kses() usa el Tag Processor. Analiza cada etiqueta, comentario y nodo de texto, pero no conoce la estructura de árbol del documento. El HTML Processor sí la construye, a cambio de bastante más tiempo y trabajo. La descripción del PR añade por qué no se cierran los elementos abiertos: mucho código del núcleo llama a wp_kses() con entradas incompletas a propósito, por ejemplo una etiqueta contenedora de apertura con parte del contenido, separada de su cierre. Aislar el contenido cerrando esos elementos estropearía sitios. Por eso las etiquetas desbalanceadas de la lista anterior siguen desbalanceadas.

#Mi array de allowed html sigue funcionando

Casi siempre, porque la lógica de permisos es la misma: una etiqueta o un atributo está en el array o no está. Lo que cambia es lo que sale alrededor. Tres patrones merecen una mirada.

  1. Arrays que permiten script o style en línea. Un shortcode que imprime un script en línea y lo pasa por wp_kses() con script permitido está justo en la zona que toca el informe. Compara la salida antigua con la nueva. Plantéate también si el script no debería ir por wp_add_inline_script() en lugar de pasar por el sanitizador.
  2. Arrays que permiten svg o math. El caso habitual son los conjuntos de iconos en línea. Si se aplica la regla del informe, la salida puede cortarse en la primera construcción que el parser no sepa tratar. Compara cada icono que entregues.
  3. Arrays para <title> y similares. <title> es un elemento especial y su contenido se trata como texto. De ahí el ejemplo con &lt;sneaky&gt; escapado.

Si tu array solo permite a, strong, em, img y unos pocos atributos, el efecto probable es cosmético: <IMG ... /> pasa a <img ...>.

#Dónde ejecuta WordPress wp_kses() sin que lo llames

El núcleo engancha el sanitizador al guardado. kses_init() está enganchada a los hooks init y set_current_user en default-filters.php y solo activa los filtros cuando el usuario actual no tiene la capacidad unfiltered_html (referencia de kses_init). En ese caso kses_init_filters() añade wp_filter_kses a title_save_pre, wp_filter_post_kses a content_save_pre, excerpt_save_pre y content_filtered_save_pre, y filtra pre_comment_content con la variante básica (referencia de kses_init_filters).

La página de referencia de wp_kses() lista además decenas de llamadas directas en el núcleo, entre ellas Walker_Comment::filter_comment_text(), wp_filter_oembed_result(), img_caption_shortcode(), filter_block_kses_value() y las pantallas de instalación de plugins y temas (referencia de wp_kses). La reescritura toca comentarios, incrustaciones oEmbed y leyendas de imagen aunque tu código no llame nunca a wp_kses().

Para las pruebas esto tiene una consecuencia práctica. En una instalación simple, administradores y editores tienen unfiltered_html; en multisitio solo los superadministradores (roles y capacidades). Su contenido no pasa por kses al guardarse. Prueba el guardado con una cuenta con el rol Autor o Colaborador, que es donde los filtros actúan, y no solo con tu cuenta de administrador.

#Qué se rompe en las pruebas que comparan HTML saneado

Todo lo que comprueba una cadena exacta. Una prueba como la primera aserción de abajo pasa hoy y puede fallar tras la reescritura, aunque el marcado signifique lo mismo. La segunda aserción sobrevive a la normalización.

<?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' ) );

Fija el comportamiento, no los bytes. En los fixtures que deben seguir siendo literales, vuelve a generarlos una vez en trunk, lee el diff como persona y haz commit del nuevo valor esperado con un comentario que diga desde qué versión cambió. Un “actualizar snapshots” en bloque es la forma en que una regresión real acaba aprobada.

Las pruebas visuales y los archivos de referencia piden el mismo cuidado. Las plantillas de correo que sanes antes de enviar son un lugar típico donde se compara una cadena guardada con una recién generada.

#Cómo usar assertEqualHTML() en las pruebas de wp_kses()

En los comentarios del informe, Jon Surrell recomienda assertEqualHTML() a quien vea fallar pruebas por las diferencias de kses. El método llegó en WordPress 6.9 como parte de WP_UnitTestCase (ticket 63527). Según el artículo de JuanMa Garrido en el blog de desarrolladores, del 26 de febrero de 2026, analiza las dos cadenas con WP_HTML_Processor y compara los árboles, no los bytes.

Ignora justo el tipo de diferencia que trae la reescritura: mayúsculas en los nombres de etiqueta, estilo de comillas, orden de atributos y de clases, las distintas formas de una referencia de caracteres y los atributos duplicados, donde gana el primero. Sigue fallando si cambia el contenido, el valor de un atributo o una clase, o si falta o sobra un atributo.

<?php
$this->assertEqualHTML(
	'<a href="/x">Read</a>',
	wp_kses_post( "<A HREF='/x'>Read</A>" )
);

El mismo artículo marca el límite: cuando importa la serialización exacta, como el contenido de un <script> o un valor concreto de data-wp-context, sigue con assertSame(). Para kses eso se traduce en assertSame() para cada caso de script o style que permitas y assertEqualHTML() para el resto. Si tu suite corre contra WordPress 6.9 o posterior, el método ya está disponible.

#Cómo pruebo en trunk sin tocar producción

Tres caminos, todos en staging o en local. Son sugerencias mías, no parte del informe oficial.

Plugin WordPress Beta Tester. Puede mover un sitio de staging a las compilaciones nocturnas y luego a las betas de 7.2. Según el calendario, la Beta 1 sale el 20 de octubre de 2026. El cambio entró en trunk el 7 de octubre, pero comprueba igualmente que el filtro wp_kses_force_legacy_parser existe en el código que instalaste.

WP-CLI nightly. En una copia desechable:

# 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.txt

wp-env. Apunta el core al espejo de trunk y monta tu plugin:

{
  "core": "WordPress/WordPress#master",
  "plugins": [ "." ]
}

Elijas el camino que elijas, termina confirmando que el filtro existe, por ejemplo con grep -rn wp_kses_force_legacy_parser wp-includes/. Si no está, no estás probando la reescritura.

#Cómo comparo la salida saneada de contenido real

Los casos sintéticos solo encuentran errores que ya imaginaste. La prueba útil es tu propia base de datos. El script de abajo sanea cada entrada publicada dos veces, una con el parser antiguo forzado y otra sin forzarlo, e imprime solo las entradas que difieren.

<?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 ) );

Ejecútalo sobre una copia de la base de datos de producción, porque los editores pegan cosas raras. Lee a mano los primeros veinte diffs. Cuenta con un puñado de causas repetidas (etiquetas autocerradas, un < suelto en una medida, SVG en línea). Corregir una causa corrige muchas entradas de golpe.

Amplía la red con el mismo patrón: sustituye wp_kses_post() por la llamada que hace tu plugin con su propio array y aliméntala con valores de opciones, texto de widgets y comentarios, no solo con entradas.

Ten en cuenta qué mide el script. Llama a wp_kses_post() sobre todo el contenido, también el que escribieron usuarios con unfiltered_html y que nunca pasó por kses al guardarse. En esas entradas el diff no describe un cambio visible hoy, sino lo que pasaría si alguien sin esa capacidad volviera a guardarlas.

#Cuándo uso la salida de emergencia del parser antiguo

El filtro se llama wp_kses_force_legacy_parser. Si devuelve true, se restablece el parser antiguo. El informe lo describe como una forma de que los dueños de sitios no ejecuten el código de reemplazo, y la palabra que importa es temporal.

<?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' );

Hay dos situaciones. La primera: encontraste una regresión en el sitio de un cliente durante la ventana de pruebas y necesitas mantenerlo estable mientras corriges el código o reportas el fallo. La segunda: un plugin que no controlas deja de funcionar y su autor no ha publicado arreglo. Pon el filtro en un mu-plugin para que sea visible en un solo sitio y asócialo a una tarea con fecha de retirada.

No lo entregues como valor permanente. La implementación antigua tiene problemas conocidos que la reescritura quiere resolver, entre ellos caídas de PCRE con valores de atributo grandes y contenido de scripts mostrado como texto visible, según la descripción del PR. Una salida silenciosa conserva esos problemas y oculta la migración al siguiente desarrollador.

El informe no promete un plazo largo. Dice que el código antiguo se mantiene por ahora por compatibilidad y que desaparecerá en futuras versiones, quizá ya en WordPress 7.2.0. Planifica como si el parser antiguo pudiera no estar en la próxima versión mayor.

Así funciona en trunk: wp_kses() aplica el filtro con true como valor por defecto, y default-filters.php registra __return_false en él, que es lo que activa el código nuevo. wp-settings.php carga los mu-plugins después de default-filters.php, así que tu __return_true con la prioridad por defecto se ejecuta más tarde y gana (kses.php en trunk). No llames directamente a wp_sanitize_html_kses(): su docblock la marca como privada y de transición.

#Rendimiento del nuevo wp_kses() frente al parser antiguo

No hay cifras publicadas. Cuando le preguntaron por benchmarks en los comentarios del informe, Dennis Snell respondió que este cambio no incluye ninguno y que la expectativa es que el código nuevo sea más rápido y use menos memoria. Da tres motivos:

  • El código antiguo parte el documento en un gran array de tokens y lo mantiene en memoria. El nuevo avanza token a token sin guardar ese array.
  • El código antiguo hace varias pasadas de procesamiento de texto sobre todo el documento. El nuevo elimina la mayoría y hace casi siempre una sola pasada hacia delante.
  • El código antiguo llama siempre a serialize_blocks( parse_blocks() ) sobre todo el documento. El nuevo solo analiza un bloque cuando de verdad hay uno y se apoya en WP_Block_Parser para retrasar aún más el trabajo.

Snell admite también que parte del procesamiento de HTML es más costosa en la implementación nueva. Si tu sitio sanea mucho contenido en cada petición, mide en staging con el filtro activado y desactivado y publica los números en los comentarios del informe, que es justo lo que pide el autor.

#Cuál es el calendario de WordPress 7.2

Del artículo del calendario de 7.2, siempre a las 15:00 UTC:

HitoFecha
Beta 120 de octubre de 2026
Beta 227 de octubre de 2026
Beta 33 de noviembre de 2026
Beta 410 de noviembre de 2026
RC117 de noviembre de 2026
RC224 de noviembre de 2026
RC31 de diciembre de 2026
Versión final8 de diciembre de 2026

El informe dice 7.2 o 7.3. No puedes contar con ninguna de las dos, pero tampoco descartar la 7.2. Un plan sensato: pasar el script de diff en staging antes de la Beta 1, de nuevo tras la RC1, y reportar en el ticket de Trac todo lo que sorprenda mientras la ventana siga abierta. Es justo la opinión que pide el autor.

#Qué debería hacer una agencia esta semana

  1. Buscar en el código wp_kses(, wp_kses_post(, wp_kses_data( y arrays $allowed propios. Anotar cuáles reciben HTML de usuarios o editores.
  2. Montar un staging en trunk por uno de los caminos anteriores y confirmar que el filtro existe.
  3. Ejecutar el script de diff contra una copia del contenido de producción. Clasificar por causa, no por entrada.
  4. Reescribir las pruebas frágiles para que comprueben estructura y regenerar una vez los fixtures de texto, con revisión humana.
  5. Revisar shortcodes y bloques que emiten script, style, SVG o MathML en línea.
  6. Decidir por cliente si hace falta la salida de emergencia, documentarla y fijar una fecha de retirada.

Si el proyecto arrastra años de saneado propio alrededor de un ecosistema de plugins, esta auditoría encaja en nuestro servicio de desarrollo WordPress a medida.

#Fuentes

Verificado el 15 de octubre de 2026 contra trunk. El comportamiento de trunk puede cambiar antes del lanzamiento.

Siguiente paso

Transforma el artículo en una implementación real

Este bloque refuerza el enlazado interno y lleva al lector al siguiente paso más útil dentro de la arquitectura del sitio.

Explora otros servicios WordPress y base de conocimiento

Refuerza tu negocio con soporte técnico profesional en áreas clave del ecosistema WordPress.

Preguntas frecuentes

Respuestas prácticas para aplicar el tema en la ejecución real.

El nuevo wp_kses() llega en WordPress 7.2#
No está decidido. El informe de Make Core habla de 7.2 o 7.3, según lo que revele el periodo de pruebas.
Cómo vuelvo al parser antiguo#
Devuelve true en el filtro wp_kses_force_legacy_parser. Es una salida temporal mientras corriges el código que depende de la salida antigua.
Se romperán mis arrays de allowed html#
El informe prevé que plugins y temas no necesiten cambios, pero la salida de algunas entradas es distinta. Compara contenido real en un build trunk.
Qué pasa con el contenido de script y style#
Cuando script o style no están permitidos, el código nuevo elimina también su contenido, no solo las etiquetas. El código antiguo lo dejaba como texto visible.

¿Necesitas un FAQ adaptado a tu sector y mercado? Preparo una versión alineada con tus objetivos de negocio.

Hablemos

Artículos Relacionados