Willow SoftwareDesarrolloSeguridad web

I found some AJAX code from 2013. 2026 security had opinions.

A Google Drive clean-out uncovered a tiny 2013 PHP and jQuery AJAX demo. In 2026 it was promptly blocked as an XSS risk, so here is the amusing before-and-after modernisation.

Publicado 2 min de lecturaGraeme Moignard
Desplácese para leer

Desarrollo • Seguridad web • Arqueología de código

Encontré código AJAX de 2013. La seguridad de 2026 tenía opiniones.

Escrito por Graeme Moignard

Publicado:

Última actualización:

Arqueología en Google Drive, al parecer

Estaba limpiando Google Drive cuando encontré un diminuto ejemplo de PHP y AJAX que escribí en 2013. Naturalmente, lo primero que hice fue intentar ejecutarlo.

La web moderna lo miró y básicamente dijo: ni hablar. La petición fue bloqueada como riesgo de cross-site scripting. Un poco brusco, pero técnicamente justo.

Lo mejor es que el Graeme de 2013 ya había identificado el problema en un comentario y luego siguió adelante sin arreglarlo.

De un vistazoUn flujo práctico de seguridadUna vista sencilla del flujo de trabajo tratado en este artículo.
01Solicitud
02Inspeccionar
03Proteger
04Registrar

La versión de 2013: jQuery, $.ajax() y optimismo

Era un ejemplo perfectamente normal para su época: jQuery 1.10.1, un POST a ajax.php, JSON de vuelta al navegador y la respuesta insertada en la página. El problema es que el valor introducido por el usuario se concatenaba directamente en HTML y después se insertaba con .html().

ajax.php originalPHP · 2013
<?php
class ajaxValidate {

	function formValidate() {
		//Put form elements into post variables (this is where you would sanitize your data)
		$field1 = @$_POST['field1'];

		//Establish values that will be returned via ajax
		$return = array();
		$return['msg'] = '';
		$return['error'] = false;

		//Begin form validation functionality
		if (!isset($field1) || empty($field1)){
			$return['error'] = true;
			$return['msg'] .= '<li>Error: Field1 is empty.</li>';
		}

		//Begin form success functionality
		if ($return['error'] === false){
			$return['msg'] = '<li>Success Message - You typed in '.$field1.'</li>';
		}

		//Return json encoded results
		return json_encode($return);
	}

}

$ajaxValidate = new ajaxValidate;
echo $ajaxValidate->formValidate();
?>
Código original del navegadorjQuery · 2013
$('#form1').submit(function(event) {
		event.preventDefault();
		$.ajax({
			type: 'POST',
			url: 'ajax.php',
			data: $(this).serialize(),
			dataType: 'json',
			success: function (data) {
				console.log(data);
				$('#info1').html(data.msg);
			}
		});
	});

Por qué 2026 protestó

La cadena vulnerable es maravillosamente corta:

POST

1. Confiar en el POST

field1 llega desde el navegador y se utiliza sin validación ni escape de salida.

PHP

2. Construir HTML con él

PHP concatena ese valor dentro de una cadena <li> y la devuelve como JSON.

.html()

3. Ejecutar el resultado

jQuery .html() indica al navegador que trate la cadena devuelta como marcado en lugar de texto plano.

Y entonces encontré este comentario…

Sí. El código contiene literalmente: “this is where you would sanitize your data”. Al parecer era tarea del Graeme del futuro. El Graeme del futuro llegó trece años después.

//Put form elements into post variables (this is where you would sanitize your data)

La versión de 2026: menos dependencias y valores predeterminados más seguros

El ejemplo actualizado hace exactamente el mismo trabajo pequeño, pero las suposiciones son muy distintas. No hay dependencia de jQuery. El navegador utiliza fetch() y FormData. PHP valida la petición y la entrada, devuelve códigos HTTP adecuados y JSON explícito, y el navegador muestra el mensaje con textContent en lugar de interpretarlo como HTML.

ajax.php modernoPHP · 2026
<?php

declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    header('Allow: POST');

    echo json_encode([
        'error' => true,
        'message' => 'Method not allowed.',
    ], JSON_THROW_ON_ERROR);

    exit;
}

$field1 = trim((string)($_POST['field1'] ?? ''));

if ($field1 === '') {
    http_response_code(422);

    echo json_encode([
        'error' => true,
        'message' => 'Field1 is empty.',
    ], JSON_THROW_ON_ERROR);

    exit;
}

if (preg_match_all('/./us', $field1, $characters) === false) {
    http_response_code(422);

    echo json_encode([
        'error' => true,
        'message' => 'Field1 must be valid UTF-8.',
    ], JSON_THROW_ON_ERROR);

    exit;
}

if (count($characters[0]) > 500) {
    http_response_code(422);

    echo json_encode([
        'error' => true,
        'message' => 'Field1 must be 500 characters or fewer.',
    ], JSON_THROW_ON_ERROR);

    exit;
}

echo json_encode([
    'error' => false,
    'message' => 'Success: you typed: ' . $field1,
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
Código moderno del navegadorJavaScript · 2026
const form = document.getElementById('example-form');
        const message = document.getElementById('message');

        form.addEventListener('submit', async (event) => {
            event.preventDefault();

            message.textContent = 'Sending…';
            message.className = '';

            try {
                const response = await fetch('ajax.php', {
                    method: 'POST',
                    headers: {
                        'Accept': 'application/json'
                    },
                    body: new FormData(form)
                });

                const data = await response.json();

                if (!response.ok) {
                    throw new Error(data.message || 'The request failed.');
                }

                // textContent is intentional: user input is rendered as text, not HTML.
                message.textContent = data.message;
                message.className = data.error ? 'error' : 'success';
            } catch (error) {
                message.textContent = error instanceof Error
                    ? error.message
                    : 'An unexpected error occurred.';
                message.className = 'error';
            }
        });

Trece años en un ejemplo diminuto

2013

2013: incluir un archivo jQuery de 93 KB para hacer una llamada AJAX, confiar en el marcado devuelto y esperar que todo el mundo se comporte.

VS
2026

2026: usar API nativas del navegador, validar en los límites, mantener los datos como datos y convertir el renderizado HTML inseguro en una decisión explícita en lugar del comportamiento predeterminado.

La prueba altamente científica

Introduce la carga clásica de abajo en la versión moderna. Debe mostrarse literalmente como texto. Si aparece una ventana emergente, algo ha salido muy mal.

<img src=x onerror=alert(1)>

Prueba aquí mismo la versión de 2026

Esta versión se ejecuta en el servidor de Willow Software, así que no hay que instalar nada. Escribe algo abajo y la página lo enviará a PHP con fetch(), recibirá JSON y actualizará el resultado sin recargar.

El resultado aparecerá aquí sin recargar la página.

¿Quieres el ejemplo moderno?

Descarga el código fuente si quieres trastear con él. A propósito solo contiene dos archivos: index.html y ajax.php. Como el ejemplo usa PHP, necesitarás PHP instalado y configurado en tu máquina, u otro servidor web compatible con PHP, para ejecutar localmente la copia descargada. Abrir index.html directamente como file:// no ejecutará PHP.

Descargar el ejemplo moderno de PHP + Fetch (.zip)

Hay algo bastante satisfactorio en encontrar código antiguo como este. No es realmente “mal código del pasado”; es un pequeño fósil que muestra lo que era habitual, práctico y aceptable en aquel momento. La web cambió, los navegadores cambiaron, las expectativas de seguridad cambiaron y, por suerte, el código también.

Además: si alguna vez te dejas un comentario que dice “this is where you would sanitize your data”, quizá convenga hacerlo antes de 2039.

Más contenido de Willow Software

Consulte el índice completo del blog para leer novedades de Salix Monitor 360, notas de seguridad y artículos sobre desarrollo web.

Abrir el blog