Como proteger AJAX y REST con nonces en PHP y JS en WordPress

Contents

Introducción

En WordPress, proteger llamadas AJAX y peticiones REST contra CSRF (Cross-Site Request Forgery) y usos no autorizados es fundamental. Las nonces (números usados una vez) son la manera estándar de WordPress para añadir una capa de protección basada en la sesión/usuario. Este artículo explica en detalle cómo generar, transportar y verificar nonces tanto para llamadas a admin-ajax.php (AJAX clásico) como para la REST API, y ofrece ejemplos prácticos en PHP y JavaScript.

¿Qué es una nonce en WordPress y cómo funciona?

Nonce en WordPress no es una clave secreta fuerte: es un token con validez limitada y asociado a un usuario y a una acción. Sirve principalmente para prevenir CSRF y operaciones accidentales. Características importantes:

  • Se genera con wp_create_nonce(action).
  • Se valida con check_ajax_referer() para AJAX o wp_verify_nonce() para usos generales.
  • Tienen una vida útil (por defecto 24 horas, calculada en ticks).
  • No sustituyen la autorización: siempre combine validación de nonce con comprobaciones de capacidades (current_user_can()).

Protección para AJAX clásico (admin-ajax.php)

1) Encolar el script y pasar la nonce

En el lado servidor (functions.php o plugin) encola tu script y crea una nonce. Se puede usar wp_localize_script o wp_add_inline_script para inyectar la nonce:

// functions.php o plugin principal
function mi_enqueue_scripts() {
    wp_enqueue_script(
        mi-script,
        plugin_dir_url(__FILE__) . assets/js/mi-script.js,
        array(jquery),
        1.0,
        true
    )

    // Crear una nonce para mi_accion_ajax
    nonce = wp_create_nonce(mi_accion_ajax)

    // Pasar datos al script
    wp_localize_script(mi-script, MiData, array(
        ajax_url =gt admin_url(admin-ajax.php),
        nonce    =gt nonce,
    ))
}
add_action(wp_enqueue_scripts, mi_enqueue_scripts)

2) Lado cliente: enviar la nonce con fetch/jQuery

En el JS, envía la nonce como campo en el body (o encabezado) para que el servidor la pueda comprobar.

// assets/js/mi-script.js
(function() {
    // Usando fetch
    function enviarDato(dato) {
        const form = new FormData()
        form.append(action, mi_accion_ajax) // nombre de la acción
        form.append(valor, dato)
        form.append(security, MiData.nonce) // nombre del campo puede ser security o el que quieras

        fetch(MiData.ajax_url, {
            method: POST,
            body: form,
            credentials: same-origin
        })
        .then(response =gt response.json())
        .then(data =gt console.log(Respuesta AJAX:, data))
        .catch(err =gt console.error(err))
    }

    // Ejemplo de uso
    // enviarDato(hola)
})()

3) Lado servidor: manejar la petición y validar la nonce

Registra los hooks AJAX y valida la nonce con check_ajax_referer(). Además, verifica capacidades si es necesario y sanea entradas antes de procesarlas o devolverlas.

// functions.php o plugin
add_action(wp_ajax_mi_accion_ajax, mi_accion_ajax_handler)
// Si también quieres que usuarios no autenticados puedan usarlo:
// add_action(wp_ajax_nopriv_mi_accion_ajax, mi_accion_ajax_handler)

function mi_accion_ajax_handler() {
    // Verificar la nonce: el primer parámetro es la acción que usaste en wp_create_nonce
    // el segundo parámetro es el nombre del campo en POST/GET: security
    check_ajax_referer(mi_accion_ajax, security)

    // Validación adicional
    if (! current_user_can(edit_posts)) {
        wp_send_json_error(array(message =gt No tienes permisos.), 403)
    }

    // Recuperar y sanitizar parámetros
    valor = isset(_POST[valor]) ? sanitize_text_field(_POST[valor]) : 

    // Procesar
    resultado = Procesado:  . valor

    wp_send_json_success(array(resultado =gt resultado))
}

Notas prácticas para AJAX

  • check_ajax_referer al fallar mata la ejecución y envía un error 403 puedes usar su tercer parámetro a false para manejar el error manualmente.
  • Para peticiones desde usuarios no autenticados, las nonces no proporcionan la misma seguridad considere alternativos (tokens, claves públicas/privadas, validación referer, etc.).
  • Siempre sanea entradas y verifica capacidades con current_user_can() antes de realizar acciones sensibles.

Protección para la REST API

Para la REST API de WordPress la forma habitual es enviar un encabezado X-WP-Nonce con el valor de wp_create_nonce(wp_rest). WordPress, por defecto, valida este encabezado para sesiones basadas en cookies y usuarios autenticados.

1) Pasar nonce para REST API

// functions.php o plugin
function mi_enqueue_rest_script() {
    wp_enqueue_script(mi-rest-script, plugin_dir_url(__FILE__) . assets/js/mi-rest.js, array(), 1.0, true)

    // Nonce especial para la REST API
    rest_nonce = wp_create_nonce(wp_rest)

    wp_localize_script(mi-rest-script, MiREST, array(
        root =gt esc_url_raw(rest_url()),
        nonce =gt rest_nonce
    ))
}
add_action(wp_enqueue_scripts, mi_enqueue_rest_script)

2) Cliente: llamar a un endpoint REST con la nonce

// assets/js/mi-rest.js
(function() {
    function llamadaREST() {
        const url = MiREST.root   mi-plugin/v1/mi-endpoint

        fetch(url, {
            method: POST,
            headers: {
                Content-Type: application/json,
                X-WP-Nonce: MiREST.nonce
            },
            body: JSON.stringify({ dato: valor }),
            credentials: same-origin // importante para cookies
        })
        .then(res =gt {
            if (!res.ok) throw new Error(HTTP error    res.status)
            return res.json()
        })
        .then(data =gt console.log(REST respuesta:, data))
        .catch(err =gt console.error(err))
    }

    // llamadaREST()
})()

3) Registrar endpoint REST y validar la nonce

Al registrar rutas en la REST API, se utiliza el parámetro permission_callback para autorizar la petición. En muchos casos basta verificar capacidades de usuario (current_user_can). Si quieres validar la nonce explícitamente, lee X-WP-Nonce con wp_verify_nonce().

// functions.php o plugin
add_action(rest_api_init, function () {
    register_rest_route(mi-plugin/v1, /mi-endpoint, array(
        methods  =gt POST,
        callback =gt mi_rest_handler,
        permission_callback =gt mi_rest_permissions_check,
    ))
})

function mi_rest_permissions_check(request) {
    // Método recomendado: comprobar capacidades
    if (! current_user_can(edit_posts)) {
        return new WP_Error(rest_forbidden, No tienes permisos para acceder., array(status =gt 403))
    }

    // Opcional: comprobar nonce directamente (ejemplo)
    headers = getallheaders()
    nonce = isset(headers[X-WP-Nonce]) ? headers[X-WP-Nonce] : null
    if (nonce  ! wp_verify_nonce(nonce, wp_rest)) {
        return new WP_Error(rest_nonce_invalid, Nonce inválida, array(status =gt 403))
    }

    return true
}

function mi_rest_handler(request) {
    params = request->get_json_params()
    dato = isset(params[dato]) ? sanitize_text_field(params[dato]) : 

    return rest_ensure_response(array(resultado =gt Recibido:  . dato))
}

Uso con endpoints públicos (no autenticados)

Si tu endpoint debe ser accesible por usuarios no autenticados, no puedes confiar en X-WP-Nonce para autenticación. Para APIs públicas considera:

  • Autenticación por token (JWT, OAuth) o clave única del plugin.
  • Comprobaciones adicionales (rate limiting, validación de origen, captchas).
  • No exponer acciones destructivas sin una capa de autenticación sólida.

Buenas prácticas y consideraciones de seguridad

  1. Usa check_ajax_referer() / wp_verify_nonce() correctamente: para AJAX utiliza check_ajax_referer, y para REST valida headers o confía en permission_callback con current_user_can.
  2. Complementa con autorización: la nonce protege contra CSRF, pero no sustituye comprobaciones de permisos. Usa current_user_can() para autorizar acciones sensibles.
  3. Sanea todas las entradas: sanitize_text_field, intval, esc_url_raw, etc., según el tipo de dato.
  4. No expongas nonces innecesariamente: pásalas solo al script que las necesita (wp_localize_script o data-attributes), y evita imprimirlas en HTML de forma insegura.
  5. Comprende sus limitaciones: las nonces caducan y no son secretas un atacante con acceso al navegador del usuario puede reutilizarlas hasta que expiren.
  6. Evita incluir nonces en URLs públicas: mejor en POST o headers.
  7. Logs y respuestas: en entorno de producción no devuelvas información sensible en errores.

Depuración y problemas comunes

  • Nonce inválida al usar REST: asegúrate de enviar X-WP-Nonce y de que la página y script estén en el mismo dominio (credentials: same-origin).
  • Chequeo falla sólo en usuarios no autenticados: recuerda que las nonces están asociadas a usuarios para visitantes anónimos considera otra estrategia.
  • Cache agresiva rompe nonces: no caches páginas con nonces dinámicas (o genera nonces por petición y evita almacenar en caché su valor visible).

Ejemplo completo mínimo

Resumen mínimo de todas las piezas: encolar, enviar y validar (AJAX).

// 1) Encolar y crear nonce
function ejemplo_enqueue() {
    wp_enqueue_script(ejemplo-js, plugin_dir_url(__FILE__) . ejemplo.js, array(), 1.0, true)
    wp_localize_script(ejemplo-js, EjemploData, array(
        ajax_url =gt admin_url(admin-ajax.php),
        nonce    =gt wp_create_nonce(ejemplo_action),
    ))
}
add_action(wp_enqueue_scripts, ejemplo_enqueue)

// 2) Handler AJAX
add_action(wp_ajax_ejemplo_action, ejemplo_handler)
function ejemplo_handler() {
    check_ajax_referer(ejemplo_action, security)
    val = isset(_POST[val]) ? sanitize_text_field(_POST[val]) : 
    wp_send_json_success(array(ok =gt true, val =gt val))
}
// ejemplo.js
(function() {
    function enviar() {
        const f = new FormData()
        f.append(action, ejemplo_action)
        f.append(val, hola mundo)
        f.append(security, EjemploData.nonce)

        fetch(EjemploData.ajax_url, { method: POST, body: f, credentials: same-origin })
            .then(r =gt r.json())
            .then(console.log)
            .catch(console.error)
    }

    // enviar()
})()

Resumen final

Las nonces de WordPress son una herramienta eficaz para mitigar CSRF en llamadas AJAX y REST cuando se usan correctamente. Siempre combínalas con comprobaciones de permisos, saneado de entradas y buenas prácticas de seguridad (no cachear, no exponer nonces en URLs, etc.). Para APIs públicas, utilice mecanismos de autenticación más adecuados (JWT, OAuth o claves de API).



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 *