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
- En el formulario, incluir un campo hidden llamado action con el nombre de la acción.
- Insertar wp_nonce_field(mi_accion, mi_nonce_field).
- Registrar dos hooks: admin_post_mi_accion y (si corresponde) admin_post_nopriv_mi_accion.
- En el handler, verificar el nonce con check_admin_referer o wp_verify_nonce y comprobar current_user_can.
- Saneamiento y validación exhaustiva del input.
- 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.
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 🙂 |