Resumen
El fallo afecta a la plataforma de alojamiento Pressable / Atomic de Automattic, en concreto al fichero atomic-platform.php. Entre otras cosas, ese código muestra un aviso en el escritorio de WordPress cuando bloquea actualizaciones excesivas de una opción, del tipo "Excess updates to option nombre_de_la_opción blocked."
El problema está en cómo se construyen y se imprimen esos avisos. Hay dos puntos débiles que se suman:
print_admin_notices()escribe el texto de cada aviso tal cual, sinesc_html()ni ninguna otra función de escape. Solo se escapa la clase CSS del contenedor:
function add_admin_notice( $message, $class = 'notice notice-error is-dismissible' ) {
$this->admin_notices[] = array( $message, $class );
}
function print_admin_notices() {
foreach ( $this->admin_notices as $notice ) {
?>
<div class="<?php echo esc_attr( $notice[1] ); ?>">
<p><?php echo $notice[0]; ?></p>
</div>
<?php
}
}
single_option_limiter_notices()lee de la tabla de opciones el array guardado enatomic_single_option_limiter_noticesy mete cada clave (el nombre de la opción bloqueada) directamente dentro del HTML del mensaje:
function single_option_limiter_notices() {
$admin_notices = get_option( $this->single_option_limiter_notices_key );
if ( empty( $admin_notices ) || ! is_array( $admin_notices ) ) {
return;
}
$changed = false;
foreach ( $admin_notices as $option => $data ) {
if ( $data['expires'] > time() ) {
$this->add_admin_notice(
"Excess updates to option <strong>$option</strong> blocked."
);
} else {
unset( $admin_notices[ $option ] );
$changed = true;
}
}
if ( $changed ) {
update_option( $this->single_option_limiter_notices_key, $admin_notices );
}
}
Según el investigador, la falta de escape en la salida probablemente es intencionada, porque los avisos llevan algo de marcado (como esa etiqueta <strong>). El problema es que el dato que viene de la base de datos tampoco se escapa al interpolarlo, así que cualquier HTML guardado en la clave acaba en la página. El resultado es un XSS almacenado en el panel de administración.
Pasos de reproducción
- Dar a la opción
atomic_single_option_limiter_noticesde un sitio este valor serializado, cuya clave es un payload y cuya fecha de caducidad está en el futuro (el timestamp1893456000corresponde al 1 de enero de 2030):
a:1:{s:34:"<script>alert('test')</script>test";a:1:{s:7:"expires";i:1893456000;}}
- Abrir con una cuenta del panel la página:
/wp-admin/plugins.php
- El script se ejecuta en la zona de avisos del administrador.
Un detalle adicional: el aviso lleva la clase is-dismissible, pero cerrarlo no sirve de nada, porque vuelve a aparecer al recargar la página.
Impacto
Es un requisito previo conseguir escribir el payload en la tabla de opciones; el reporte no explica por qué vía. A partir de ahí, el JavaScript se ejecuta cuando otros usuarios visitan la página afectada del panel. El título del reporte afirma que el fallo lleva a ejecución de código, aunque el texto no detalla esa cadena.
El reporte se envió el 1 de diciembre de 2025, figura como resuelto y se hizo público el 18 de febrero de 2026. No consta recompensa, CWE ni CVE.
Remediación
El investigador propuso escapar la salida en print_admin_notices() o, si se quiere conservar el marcado de los avisos, escapar el dato antes de construir el mensaje:
$this->add_admin_notice(
sprintf( 'Excess updates to option <strong>%s</strong> blocked.', esc_html( $option ) );
);
El reporte no describe qué cambio concreto se aplicó finalmente.
Qué aprender de este caso
- Cuando una función de avisos o mensajes del panel imprime HTML "de confianza" sin escapar, revisa todos los sitios que la llaman: basta con que uno interpole un valor leído de la base de datos (aquí, la clave de un array guardado con
get_option()) para convertirla en un XSS almacenado. - Si el mensaje necesita marcado fijo como
<strong>, deja la plantilla como literal y escapa solo las variables conesc_html()dentro de unsprintf(), en vez de interpolarlas directamente en una cadena con comillas dobles. - Las claves de los arrays serializados son tan manipulables como sus valores: en auditorías de plugins de WordPress, sigue también los
foreach ( $x as $clave => $valor )que acaban en la salida. - Un fallo que exige escribir en la base de datos no es inofensivo: en sitios con varios usuarios del panel, o donde otro fallo permita modificar opciones, sirve para llegar al navegador de quien tenga más permisos.