Google Drive archaeology, apparently
I was cleaning out Google Drive and found a tiny PHP and AJAX example I wrote back in 2013. Naturally, the first thing I did was try to run it.
The modern web took one look at it and effectively said: absolutely not. The request was blocked as a cross-site scripting risk. Slightly rude, but technically fair.
The best part is that 2013-me had already identified the problem in a comment and then wandered off without actually fixing it.
The 2013 version: jQuery, $.ajax() and optimism
This was a perfectly normal little example for its era: jQuery 1.10.1, a POST to ajax.php, JSON back to the browser and the response dropped into the page. The trouble is that the value supplied by the user was concatenated directly into HTML and then inserted with .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);
}
});
});
Why 2026 objected
The vulnerable chain is wonderfully short:
1. Trust the POST
field1 arrives from the browser and is used without validation or output escaping.
2. Build HTML with it
PHP concatenates that value into an <li> string and sends it back as JSON.
3. Execute the result
jQuery .html() tells the browser to treat the returned string as markup rather than plain text.
The 2026 version: fewer dependencies, safer defaults
The updated example does the same tiny job, but the assumptions are very different. There is no jQuery dependency. The browser uses fetch() and FormData. PHP validates the request and input, returns sensible HTTP status codes and explicit JSON, and the browser renders the returned message with textContent rather than interpreting it as 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';
}
});
Thirteen years in one tiny example
2013: include a 93 KB jQuery file to make an AJAX call, trust the returned markup and hope everyone behaves.
2026: use native browser APIs, validate at the boundary, keep data as data, and make unsafe HTML rendering an explicit choice rather than the default.
The very scientific test
Put the classic payload below into the modern version. It should be displayed literally as text. If a box pops up, something has gone badly wrong.
<img src=x onerror=alert(1)>Try the 2026 version right here
This one is running on the Willow Software server, so there is nothing to install. Type something below and the page will POST it to PHP with fetch(), receive JSON back and update the result without reloading.
Want the modern example?
Download the source if you want to poke around with it. It is deliberately just two files: index.html and ajax.php. Because the example uses PHP, you will need PHP installed and configured on your machine, or another PHP-enabled web server, if you want to run the downloaded copy locally. Opening index.html directly as a file:// page will not execute PHP.
There is something quite satisfying about finding old code like this. It is not really “bad code from the past”; it is a tiny fossil showing what was common, convenient and acceptable at the time. The web changed, browsers changed, security expectations changed, and thankfully so did the code.
Also: if you ever leave yourself a comment saying “this is where you would sanitise your data”, perhaps consider doing it before 2039.
And then I found this comment…
Yes. The source literally contains: “this is where you would sanitize your data”. Apparently that was a job for Future Graeme. Future Graeme arrived thirteen years later.
//Put form elements into post variables (this is where you would sanitize your data)