Como evitar XSS en shortcodes y metaboxes con PHP en WordPress

Contents

Introducción

Este artículo explica, con todo lujo de detalles, cómo evitar vulnerabilidades de Cross-Site Scripting (XSS) cuando se desarrollan shortcodes y metaboxes en WordPress usando PHP. Incluye buenas prácticas, funciones nativas, ejemplos completos de código y recomendaciones operativas. El objetivo es que al implementar estas técnicas se reduzca al máximo el riesgo de inyección de contenido malicioso desde datos de usuarios, atributos de shortcodes o campos de metaboxes.

Principios fundamentales

  • Sanitizar al guardar, escapar al mostrar: Cuando se reciben datos del usuario (POST, atributos de shortcode, campos personalizados) hay que sanitizarlos antes de guardarlos en la base de datos. Cuando se muestran esos datos, hay que escaparlos según el contexto (HTML, atributo, URL, JavaScript, etc.).
  • Validar capacidad y verificar nonces: En metaboxes y endpoints que modifican datos asegúrate de verificar la intención (nonce) y de que el usuario tiene permiso (current_user_can).
  • Principio de menor privilegio: No permitir más HTML del estrictamente necesario. Si se van a permitir etiquetas HTML, definir una whitelist con wp_kses o usar funciones seguras como wp_kses_post cuando sea apropiado.
  • No confiar en los datos de entrada: Los atributos de shortcodes y campos enviados desde el cliente pueden ser manipulados.

¿Sanitizar o escapar?

  • Sanitizar (sanitize_): transformar o filtrar valores cuando se reciben, antes de guardarlos.
  • Escapar (esc_ o wp_kses_): limpiar o codificar al imprimir en HTML para evitar que se interprete como código.

Funciones clave y cuándo usarlas

sanitize_text_field() Eliminar caracteres peligrosos en texto plano (guardar en DB).
sanitize_textarea_field() Como sanitize_text_field pero para áreas de texto multilínea.
sanitize_email(), sanitize_key(), sanitize_title_with_dashes() Funciones específicas para ciertos tipos de datos.
esc_html() Escapar datos cuando se imprimen dentro de HTML.
esc_attr() Escapar datos para atributos HTML (value, title, data-).
esc_url(), esc_url_raw() Sanitizar o escapar URLs según si se van a mostrar o guardar.
wp_kses(), wp_kses_post() Permitir un conjunto controlado de etiquetas HTML (whitelist).
wp_nonce_field(), wp_verify_nonce() Proteger acciones enviadas por formularios.

Shortcodes seguros: estructura y ejemplo

Los shortcodes reciben atributos y contenido. Debes validar y sanitizar los atributos, y decidir si permites HTML en el contenido. Nunca imprimir datos sin escapar.

Ejemplo: shortcode que genera un botón con URL y etiqueta

Objetivos del ejemplo: recibir atributos url y label, permitir opcionalmente HTML limitado en la etiqueta (por ejemplo, y ), y escapar todo correctamente al mostrar.

 ,
        label => Clic aquí,
        new_tab => false,
    ), atts, mi_boton)

    // Sanitizar atributos básicos
    url = isset(atts[url]) ? esc_url_raw(atts[url]) : 
    label = isset(atts[label]) ? sanitize_text_field(atts[label]) : 
    new_tab = (atts[new_tab] === true) ? true : false

    // Si se proporciona contenido entre apertura/cierre de shortcode, opcionalmente permitir HTML limitado
    if (!empty(content)) {
        // Permitir solo etiquetas simples dentro del contenido del shortcode
        allowed_tags = array(
            b => array(),
            i => array(),
            strong => array(),
            em => array(),
        )
        content = wp_kses(content, allowed_tags)
        // O si queremos tratarlo como texto plano:
        // content = sanitize_text_field(content)
    }

    // Escapar al imprimir. esc_url para URL en HTML, esc_attr/esc_html para atributos/texto
    target = new_tab ?  target=_blank rel=noopener noreferrer : 

    // Construir salida con escapes apropiados
    label_output = !empty(content) ? content : esc_html(label)
    url_output = esc_url(url)

    return  . label_output . 
}
?>

Notas sobre el ejemplo

  • Se usa esc_url_raw() cuando se sanitiza para almacenarse o procesarse al imprimir se usa esc_url() y esc_attr() para atributos.
  • Si permites HTML en el contenido, usa wp_kses() con una whitelist y evita permitir JavaScript o atributos event-handler (onclick, onmouseover).
  • Para atributos booleanos usa checks estrictos (comparar con true, 1 o comprobar isset).

Metaboxes seguros: creación, renderizado y guardado

En metaboxes debes validar la petición (nonce), comprobar permisos, sanitizar los datos antes de guardar y escapar al mostrar los valores en los campos del metabox.

Ejemplo: metabox con un campo de texto y un textarea seguro

ID, _mi_campo_texto, true)
    campo_area = get_post_meta(post->ID, _mi_campo_area, true)

    // Escapar al imprimir en los inputs (contexto atributo y textarea)
    echo 
echo echo
echo } // Guardado seguro add_action(save_post, function(post_id) { // Evitar autosaves if (defined(DOING_AUTOSAVE) DOING_AUTOSAVE) { return } // Verificar nonce if (!isset(_POST[mi_metabox_nonce]) !wp_verify_nonce(_POST[mi_metabox_nonce], mi_guardado_metabox)) { return } // Verificar permisos del usuario if (!current_user_can(edit_post, post_id)) { return } // Sanitizar y actualizar campos if (isset(_POST[mi_campo_texto])) { // Limpia el texto plano texto = sanitize_text_field(_POST[mi_campo_texto]) update_post_meta(post_id, _mi_campo_texto, texto) } else { delete_post_meta(post_id, _mi_campo_texto) } if (isset(_POST[mi_campo_area])) { // Permitir HTML limitado o texto según necesidad // Opción A: texto plano // area = sanitize_textarea_field(_POST[mi_campo_area]) // Opción B: permitir HTML limitado allowed_tags = array( a => array(href => array(), title => array()), b => array(), i => array(), strong => array(), em => array(), br => array(), p => array(), ) area = wp_kses(_POST[mi_campo_area], allowed_tags) update_post_meta(post_id, _mi_campo_area, area) } else { delete_post_meta(post_id, _mi_campo_area) } }) ?>

Explicaciones del flujo de guardado

  1. Comprobar autosave para no sobrescribir datos en operaciones automáticas.
  2. Verificar nonce para evitar CSRF.
  3. Verificar permisos del usuario con current_user_can.
  4. Sanitizar datos recibidos antes de llamar a update_post_meta.
  5. Si permites HTML, usa wp_kses con una whitelist explícita no uses wp_kses_post si no sabes exactamente qué etiquetas quieres permitir.

Escenarios peligrosos y cómo mitigarlos

  • Insertar HTML no filtrado: Nunca uses echo input_raw sin escapar. Usa wp_kses o esc_html según el caso.
  • URLs sin validar: Los atributos href/src pueden apuntar a javascript: — usa esc_url() y opcionalmente comprobar el esquema.
  • Atributos data- o event handlers: No permitir atributos personalizados sin validar. Evitar atributos que comiencen con on (onclick).
  • Guardar datos listas o JSON: Sanitizar cada elemento y usar wp_json_encode al renderizar datos para JavaScript.

Permitir HTML limitado: ejemplo de whitelist con wp_kses

 array(
        href => true,
        title => true,
        rel => true,
    ),
    br => array(),
    em => array(),
    strong => array(),
    b => array(),
    i => array(),
    p => array(),
)

// Uso
entrada = _POST[descripcion] // ejemplo
limpia = wp_kses(entrada, allowed)
?>

Buenas prácticas adicionales

  • Centralizar sanitización y escaping en funciones reutilizables para reducir errores.
  • Si tu shortcode genera HTML complejo, construirlo con funciones PHP que realicen escapes en cada fragmento en lugar de concatenar grandes bloques sin comprobar.
  • Auditar dependencias y plugins de terceros que puedan filtrar o modificar output (filtros en the_content, etc.).
  • Hacer pruebas con payloads de XSS conocidos para verificar que el contenido se neutraliza correctamente.
  • Considerar cabeceras HTTP como Content-Security-Policy (CSP) para añadir capas extra de protección frente a la ejecución de scripts no autorizados.

Resumen

Para evitar XSS en shortcodes y metaboxes en WordPress sigue estas reglas claras: sanitizar datos al guardar, escapar al mostrar según contexto, verificar nonces y permisos, y permitir solo el HTML estrictamente necesario con wp_kses si hace falta. Usa las funciones WordPress diseñadas para esto (sanitize_, esc_, wp_kses, wp_nonce_field, wp_verify_nonce). Con estas prácticas reducirás significativamente la superficie de ataque y mejorarás la seguridad de tu sitio.

Fin del artículo



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *