Como proteger formularios con nonces y capabilities en PHP en WordPress

Contents

Introducción

Los formularios son una superficie frecuente de ataque en cualquier aplicación web. En WordPress, proteger formularios significa principalmente dos cosas: prevenir CSRF (Cross-Site Request Forgery) mediante nonces y restringir qué usuarios pueden ejecutar ciertas acciones mediante comprobaciones de capabilities. Este artículo explica en detalle cómo combinar nonces y capacidades en PHP para asegurar formularios en WordPress, con ejemplos prácticos para formularios front-end, admin y llamadas AJAX.

Conceptos clave

  • Nonce: en WordPress es un valor único temporal que ayuda a verificar el origen de una petición y mitigar CSRF. No es una token criptográfico permanente su función es impedir que peticiones externas no autorizadas se ejecuten.
  • Capability: permiso que tiene un usuario (o rol) para realizar una acción, ver o modificar recursos. Se controla con funciones como current_user_can() y se configuran en roles con add_cap() y remove_cap().
  • Sanitización y escape: validar y limpiar los datos de entrada con funciones como sanitize_text_field(), y escapar la salida con esc_html(), esc_attr(), etc.

Por qué combinar nonces y capabilities

Un nonce protege que la acción provenga de tu sitio (reduce CSRF), pero no garantiza que el usuario tenga permiso para realizar la acción. Una comprobación de capability garantiza que solo usuarios autorizados puedan realizar operaciones privilegiadas. Ambos mecanismos son complementarios y deben usarse juntos para operaciones sensibles (crear, editar o borrar contenido, cambiar opciones, etc.).

Buenas prácticas generales

  1. Generar un nonce por acción y por formulario (no reutilizar indefinidamente).
  2. Verificar nonce en el procesamiento antes de cualquier operación de escritura.
  3. Comprobar capabilities antes de procesar o mostrar formularios que realizan acciones privilegiadas.
  4. Sanitizar todos los datos recibidos y escapar siempre la salida.
  5. En AJAX, emplear check_ajax_referer() y devolver respuestas con códigos HTTP apropiados.
  6. No confiar únicamente en input hidden de HTML para decisiones de seguridad.

Funcionamiento básico de nonces en WordPress

  • Generación: wp_create_nonce(mi-accion) o implícitamente mediante wp_nonce_field(mi-accion, mi_nonce) que genera input hidden en un formulario.
  • Validación: wp_verify_nonce(_REQUEST[mi_nonce], mi-accion) o funciones helper como check_admin_referer(mi-accion, mi_nonce) y check_ajax_referer(mi-accion, mi_nonce, true).
  • Caducidad: por defecto suele durar 12-24 horas según configuración y contexto.

Ejemplo 1 — Formulario front-end (envío normal)

Este ejemplo muestra cómo generar un formulario con nonce en el front-end y cómo procesarlo en una función hookeada a init o a una ruta propia. Incluye comprobación de capability y sanitización.

/ 1) Mostrar el formulario en el front-end (shortcode o plantilla) /
function mi_formulario_frontend() {
    if ( ! is_user_logged_in() ) {
        return ltpgtDebes iniciar sesión para enviar este formulario.lt/pgt
    }

    // Comprobamos capability: por ejemplo edit_posts para permitir a autores y editores
    if ( ! current_user_can( edit_posts ) ) {
        return ltpgtNo tienes permiso para enviar este formulario.lt/pgt
    }

    ob_start()
    ?gt
    ltform method=post action=lt?php echo esc_url( admin_url(admin-post.php) ) ?gtgt
        ltpgt
            ltlabelgtTítulo:lt/labelgt
            ltinput type=text name=mi_titulo required /gt
        lt/pgt
        lt?php wp_nonce_field( mi_form_action, mi_form_nonce ) ?gt
        ltinput type=hidden name=action value=procesar_mi_form /gt
        ltbutton type=submitgtEnviarlt/buttongt
    lt/formgt
    lt?php
    return ob_get_clean()
}
add_shortcode( mi_form, mi_formulario_frontend )

/ 2) Procesar el formulario (admin-post.php) /
function procesar_mi_form() {
    // Verificar el nonce y que el usuario tenga capability
    if ( ! isset( _POST[mi_form_nonce] )  ! wp_verify_nonce( _POST[mi_form_nonce], mi_form_action ) ) {
        wp_die( Nonce no válido. )
    }

    if ( ! is_user_logged_in()  ! current_user_can( edit_posts ) ) {
        wp_die( No tienes permiso. )
    }

    // Sanitizar entrada
    titulo = isset( _POST[mi_titulo] ) ? sanitize_text_field( wp_unslash( _POST[mi_titulo] ) ) : 

    if ( empty( titulo ) ) {
        wp_redirect( wp_get_referer() ? wp_get_referer() : home_url() )
        exit
    }

    // Ejemplo: crear un post privado
    post_id = wp_insert_post( array(
        post_title   => titulo,
        post_status  => private,
        post_type    => post,
        post_author  => get_current_user_id(),
    ) )

    if ( is_wp_error( post_id ) ) {
        wp_die( Error al crear el post. )
    }

    wp_safe_redirect( add_query_arg( success, 1, wp_get_referer() ) )
    exit
}
add_action( admin_post_nopriv_procesar_mi_form, procesar_mi_form ) // si permites no logueados
add_action( admin_post_procesar_mi_form, procesar_mi_form ) // para usuarios logueados

Explicación del anterior

  • Se usa admin-post.php como punto de entrada estándar para procesar formularios enviados desde front-end o admin.
  • Se llama a wp_nonce_field para generar el campo hidden y wp_verify_nonce para validar al procesar.
  • current_user_can se comprueba tanto al mostrar como al procesar (defensa en profundidad).
  • Se sanea la entrada con sanitize_text_field y se usa wp_unslash para quitar slashes añadidos por WP.

Ejemplo 2 — Formulario en el área de administración

En páginas de administración es común usar check_admin_referer que combina verificación de nonce y referer.

/ Mostrar la página de ajustes /
function mi_menu_admin() {
    add_menu_page( Mi plugin, Mi plugin, manage_options, mi-plugin, mi_plugin_pagina )
}
add_action( admin_menu, mi_menu_admin )

function mi_plugin_pagina() {
    if ( ! current_user_can( manage_options ) ) {
        wp_die( Acceso denegado. )
    }
    ?>
    ltform method=post action=gt
        lt?php wp_nonce_field( mi_ajuste_guardar, mi_ajuste_nonce ) ?gt
        ltinput type=text name=mi_ajuste value=lt?php echo esc_attr( get_option(mi_ajuste, ) ) ?gt /gt
        ltbutton type=submit name=guardar_mi_ajustegtGuardarlt/buttongt
    lt/formgt
    

Ejemplo 3 — AJAX protegido con nonces y capabilities

Para peticiones AJAX en WordPress (admin-ajax.php o endpoints REST) hay helpers específicos. Aquí se muestra admin-ajax.php.

/ Encolar script y pasar nonce /
function mi_enqueue_scripts() {
    wp_enqueue_script( mi-ajax, plugin_dir_url( __FILE__ ) . mi-ajax.js, array(jquery), 1.0, true )
    wp_localize_script( mi-ajax, MiAjax, array(
        ajax_url => admin_url( admin-ajax.php ),
        nonce    => wp_create_nonce( mi_ajax_action ),
    ) )
}
add_action( wp_enqueue_scripts, mi_enqueue_scripts )

/ Handler AJAX para usuarios logueados /
function mi_ajax_handler() {
    // check_ajax_referer mata la ejecución (wp_die) si nonce inválido
    check_ajax_referer( mi_ajax_action, security )

    if ( ! is_user_logged_in()  ! current_user_can( edit_posts ) ) {
        wp_send_json_error( array( message => No autorizado ), 403 )
    }

    dato = isset( _POST[dato] ) ? sanitize_text_field( wp_unslash( _POST[dato] ) ) : 

    // Procesa y responde
    wp_send_json_success( array( received => dato ) )
}
add_action( wp_ajax_mi_accion_ajax, mi_ajax_handler ) // para logueados

/ Handler AJAX para usuarios no logueados (si procede)
add_action( wp_ajax_nopriv_mi_accion_ajax, mi_ajax_handler )
/

En el lado cliente (ejemplo breve de JS para enviar AJAX)

jQuery.post(MiAjax.ajax_url, {
    action: mi_accion_ajax,
    security: MiAjax.nonce,
    dato: valor
}, function(response) {
    if ( response.success ) {
        console.log(OK, response.data)
    } else {
        console.error(Error, response)
    }
})

Capabilities personalizadas

A veces necesitas capacidades propias para controlar acciones precisas. Un patrón típico es registrar capacidades al activar el plugin y asignarlas a roles concretos.

/ Al activar el plugin asignar capacidad personalizada /
function mi_plugin_activar() {
    role = get_role( administrator )
    if ( role ) {
        role->add_cap( mi_capacidad_personalizada )
    }
}
register_activation_hook( __FILE__, mi_plugin_activar )

/ Uso en código /
if ( ! current_user_can( mi_capacidad_personalizada ) ) {
    wp_die( No autorizado a usar esta funcionalidad. )
}

Consideraciones de seguridad y detalles finos

  • Nonces no sustituyen a HTTPS: siempre usar HTTPS para proteger tráfico y cookies.
  • Nonces no son tokens anti-replay infalibles: sirven para mitigar CSRF, pero no reemplazan controles adicionales en operaciones de alta seguridad.
  • Verificación doble: comprobar capa de autorización (capability) tanto al mostrar el formulario como al procesarlo.
  • Evitar confiar en inputs HTML: no uses campos ocultos para decidir permisos.
  • REST API: en endpoints REST usa permisos propios (permission_callback) y comprueba nonces si trabajas desde front-end con wp_rest nonce (X-WP-Nonce header o utilizando wp_create_nonce(wp_rest)).
  • Mensajes de error: evita revelar detalles técnicos en mensajes públicos devuelve errores genéricos y registra detalles en logs cuando proceda.

Lista de verificación antes de desplegar

  1. ¿Todos los formularios que realizan cambios tienen un nonce único y se verifica al procesar?
  2. ¿Se comprueban las capabilities antes de mostrar y antes de procesar?
  3. ¿Se sanitizan todos los inputs y se escapa la salida?
  4. ¿Se usa check_admin_referer, check_ajax_referer o wp_verify_nonce según convenga?
  5. ¿Se manejan correctamente códigos y respuestas en AJAX (códigos HTTP y JSON)?
  6. ¿Se revisaron roles y capacidades asignadas al activar el plugin o tema?

Resumen

Proteger formularios en WordPress es un proceso en capas: nonces para mitigar CSRF, comprobaciones de capabilities para autorizar acciones, y sanitización/escape para proteger la integridad de los datos. Implementa ambos mecanismos y sigue las prácticas descritas en este artículo para reducir significativamente la superficie de ataque de tus formularios y endpoints.



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 *