Willow SoftwareDevelopmentWeb security

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.

Published 2 min readGraeme Moignard
Scroll to read

Development • Web security • Code archaeology

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

Written by Graeme Moignard

Published:

Last updated:

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.

At a glanceA practical security workflowA simple view of the workflow discussed in this article.
01Request
02Inspect
03Protect
04Record

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().

Original ajax.phpPHP · 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();
?>
Original browser codejQuery · 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);
			}
		});
	});

Why 2026 objected

The vulnerable chain is wonderfully short:

POST

1. Trust the POST

field1 arrives from the browser and is used without validation or output escaping.

PHP

2. Build HTML with it

PHP concatenates that value into an <li> string and sends it back as JSON.

.html()

3. Execute the result

jQuery .html() tells the browser to treat the returned string as markup rather than plain text.

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)

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.

Modern ajax.phpPHP · 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);
Modern browser codeJavaScript · 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';
            }
        });

Thirteen years in one tiny example

2013

2013: include a 93 KB jQuery file to make an AJAX call, trust the returned markup and hope everyone behaves.

VS
2026

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.

The result will appear here without reloading the page.

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.

Download the modern PHP + Fetch example (.zip)

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.

More from Willow Software

Browse the full blog index for Salix Monitor 360 updates, security notes and web development articles.

Open blog index