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.
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().
<?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();
?>
$('#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:
1. Confiar en el POST
field1 llega desde el navegador y se utiliza sin validación ni escape de salida.
2. Construir HTML con él
PHP concatena ese valor dentro de una cadena <li> y la devuelve como JSON.
3. Ejecutar el resultado
jQuery .html() indica al navegador que trate la cadena devuelta como marcado en lugar de texto plano.
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.
<?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);
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: 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.
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.
¿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.
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.
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)