Como evitar CSRF en acciones de admin-post.php en WordPress

Contents

Cómo evitar CSRF en acciones que usan admin-post.php en WordPress

El archivo admin-post.php es una forma habitual en WordPress para procesar formularios tanto del panel de administración como del frontend. Sin embargo, al tratarse de puntos de entrada que ejecutan lógica del servidor, son objetivos naturales para ataques CSRF (Cross-Site Request Forgery). Este artículo explica con todo detalle cómo prevenir CSRF en handlers registrados con admin-post.php: generación y verificación de nonces, comprobaciones de capacidad, saneamiento de datos, uso correcto de métodos HTTP y prácticas recomendadas adicionales.

Qué es CSRF y por qué importa en admin-post.php

CSRF es un ataque donde un sitio malicioso hace que el navegador autenticado de una víctima envíe una petición no deseada a otra web (por ejemplo, tu sitio WordPress) que confía en las cookies de sesión. Si tu handler en admin-post.php realiza acciones sensibles (crear, borrar, modificar recursos) sin protección, un tercero puede provocar esas acciones sin que el propietario lo desee.

Resumen de la defensa

  • Usar nonces de WordPress (wp_nonce_field / wp_verify_nonce / check_admin_referer) para asegurarse de que la solicitud fue generada por tu sitio.
  • Forzar uso de POST para operaciones con efecto (no usar GET).
  • Comprobar permisos con current_user_can.
  • Saneamiento y validación estricta de entradas (sanitize_text_field, intval, sanitize_email, etc.).
  • Evitar exponer URIs sensibles sin protección y usar wp_nonce_url para enlaces que hagan acciones por GET (si no hay alternativa mejor).
  • Usar redirecciones seguras (wp_safe_redirect) y siempre terminar con exit después de redirigir.

Funciones clave de WordPress y su propósito

wp_nonce_field Genera campo hidden y valor nonce en formularios.
check_admin_referer Verifica nonce y, opcionalmente, el referer. Útil en handlers que reciben POST.
wp_verify_nonce Verifica nonce manualmente (útil para GET o comprobaciones más personalizadas).
wp_nonce_url Añade nonce a una URL (uso con cuidado cambiar datos por GET no es recomendado para acciones destructivas).
current_user_can Verifica permisos del usuario actual.
wp_safe_redirect Redirige a una URL permitida (protege contra redirecciones abiertas).

Pauta general para implementar un handler seguro

  1. En el formulario, incluir un campo hidden llamado action con el nombre de la acción.
  2. Insertar wp_nonce_field(mi_accion, mi_nonce_field).
  3. Registrar dos hooks: admin_post_mi_accion y (si corresponde) admin_post_nopriv_mi_accion.
  4. En el handler, verificar el nonce con check_admin_referer o wp_verify_nonce y comprobar current_user_can.
  5. Saneamiento y validación exhaustiva del input.
  6. Procesar la acción y redirigir con wp_safe_redirect seguido de exit.

Ejemplo completo: formulario y handler seguro (POST)

Formulario que envía datos a admin-post.php y usa nonce.

method=post enctype=multipart/form-data>

Registro del handler y la función que procesa la petición (en tu plugin o functions.php).

add_action(admin_post_mi_accion_handle, mi_accion_handle_callback)
// Si quieres permitir usuarios no autenticados (por ejemplo un formulario público):
add_action(admin_post_nopriv_mi_accion_handle, mi_accion_handle_callback)

function mi_accion_handle_callback() {
    // 1) Verificar método: forzar POST para operaciones que cambian estado
    if ( ! wp_doing_ajax()  _SERVER[REQUEST_METHOD] !== POST ) {
        wp_die(Método no permitido, Error, array(response => 405))
    }

    // 2) Verificar nonce (verifica también el referer por defecto)
    check_admin_referer(mi_accion_nonce, mi_accion_nonce_field)
    // Alternativa si quieres control más fino:
    // if ( ! isset(_POST[mi_accion_nonce_field])  ! wp_verify_nonce(_POST[mi_accion_nonce_field], mi_accion_nonce) ) { wp_die(Nonce inválido, Error, array(response => 403)) }

    // 3) Comprobar capacidades (si aplica)
    if ( ! current_user_can(edit_posts) ) {
        wp_die(No autorizado, Error, array(response => 403))
    }

    // 4) Saneamiento de datos
    nombre = isset(_POST[nombre]) ? sanitize_text_field(wp_unslash(_POST[nombre])) : 

    // 5) Lógica del negocio (por ejemplo guardar un post o meta)
    // ... tu código seguro aquí ...

    // 6) Redirigir con seguridad
    redirect_to = wp_get_referer() ? wp_get_referer() : admin_url()
    redirect_to = add_query_arg(mensaje, ok, redirect_to)
    wp_safe_redirect( redirect_to )
    exit
}

Ejemplo: Cuando necesitas un enlace (GET) para una acción — evitar si es posible

Las operaciones que modifican estado deberían usar POST. Si no hay alternativa y se usa GET (por ejemplo, un enlace de confirmación), añade un nonce y verifica.

// Generar URL segura con nonce
delete_url = wp_nonce_url(
    add_query_arg(
        array(
            action => borrar_item,
            item   => item_id,
        ),
        admin_url(admin-post.php)
    ),
    borrar_item_ . item_id, // acción del nonce
    borrar_item_nonce        // nombre del campo nonce en la URL
)

// En la salida HTML:
echo Borrar

En el handler:

add_action(admin_post_borrar_item, borrar_item_handler)

function borrar_item_handler() {
    // Validar presencia de los parámetros
    item_id = isset(_GET[item]) ? intval(_GET[item]) : 0

    // Verificar nonce para GET
    if ( ! isset(_GET[borrar_item_nonce])  ! wp_verify_nonce( _GET[borrar_item_nonce], borrar_item_ . item_id ) ) {
        wp_die(Nonce inválido, Error, array(response => 403))
    }

    // Comprobar permisos
    if ( ! current_user_can(delete_posts) ) {
        wp_die(No autorizado, Error, array(response => 403))
    }

    // Realizar borrado y redirigir
    // ...
    wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url() )
    exit
}

Protección adicional y buenas prácticas

  • Preferir POST para cambios de estado. Evitar acciones destructivas por GET.
  • Usar check_admin_referer cuando el formulario viene del admin o del frontend para aprovechar la comprobación del referer además del nonce.
  • Verificar capacidades con current_user_can antes de ejecutar acciones sensibles.
  • Saneamiento y escaping: siempre sanitize_ al guardar y esc_ al imprimir.
  • Tiempo de vida de nonces: los nonces de WP tienen un tick (por defecto 12 horas). No son una semilla criptográfica de un solo uso a largo plazo. Para operaciones extremadamente sensibles considera añadir comprobaciones adicionales (por ejemplo token único en DB con expiración corta).
  • Evitar mostrar información sensible en URLs y evitar incluir tokens en enlaces públicos que puedan filtrarse en logs.
  • Para subidas de archivos validar tipo MIME, usar wp_handle_upload y cambiar permisos según corresponda.
  • Content-Security-Policy y SameSite cookies ayudan como defensa en profundidad contra ciertos vectores de CSRF y robo de cookies.

Ejemplo rápido: subida de archivo con comprobaciones

add_action(admin_post_upload_document, upload_document_handler)

function upload_document_handler() {
    if ( _SERVER[REQUEST_METHOD] !== POST ) {
        wp_die(Método no permitido, Error, array(response => 405))
    }

    check_admin_referer(upload_document_nonce, upload_document_nonce_field)

    if ( ! current_user_can(upload_files) ) {
        wp_die(No autorizado, Error, array(response => 403))
    }

    if ( empty(_FILES[document])  _FILES[document][error] !== UPLOAD_ERR_OK ) {
        wp_die(Archivo no válido, Error, array(response => 400))
    }

    file = _FILES[document]

    // Utilizar las utilidades de WP para manejar la subida de forma segura
    require_once ABSPATH . wp-admin/includes/file.php
    overrides = array( test_form => false ) // ya hemos verificado el nonce
    uploaded = wp_handle_upload( file, overrides )

    if ( isset(uploaded[error]) ) {
        wp_die(Error al subir:  . esc_html(uploaded[error]), Error, array(response => 500))
    }

    // Procesar uploaded[file] o uploaded[url] según convenga

    wp_safe_redirect( wp_get_referer() )
    exit
}

Resumen final

Proteger los endpoints que usan admin-post.php frente a CSRF es fundamental. La combinación correcta es: nonces (wp_nonce_field / check_admin_referer), comprobación de permisos con current_user_can, uso de POST para operaciones que cambian estado, saneamiento de datos y redirecciones seguras. Los nonces de WordPress brindan una defensa eficaz y simple de implementar, pero deben acompañarse de otras buenas prácticas como defensa en profundidad y evitar exponer acciones sensibles por GET.



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 *