PHPPreventing XSS

Preventing Cross-Site Scripting (XSS)

Cross-site scripting, almost always shortened to XSS, is what happens when an application takes text that came from a user and outputs it into a page without neutralizing it first, letting an attacker's own HTML and JavaScript run inside someone else's browser session. The attacker's script then executes with the same privileges as the real page — it can read the page's cookies, make requests using the victim's active session, redraw the page to phish for a password, or silently forward form data somewhere else. The vulnerability lives entirely in the output step: it does not matter how the malicious text got into your system, only that it was echoed back without being escaped for the context it landed in.

Reflected vs stored XSS

The two attacks differ only in where the malicious text lives before it reaches a victim's browser. Reflected XSS bounces straight off a single request — an attacker crafts a link containing a script in a query parameter, a search box, or an error message, and sends that link to a victim. The server reads the parameter, echoes it back into the response for that one request, and the script executes immediately for whoever clicked the link. Stored XSS is more dangerous because the payload is saved somewhere persistent — a comment, a profile bio, a product review — and then rendered for every visitor who later views that page, with no need to trick anyone into clicking a special link at all.

Vulnerable — echoing input unescaped
Vulnerable — do not use
This search page prints the search term straight back into the page so it can be shown next to the results. It is shown only to explain the exploit — never output user input this way.

search.php (vulnerable)

PHP
<?php
$term = $_GET['q'];
echo "<h1>Search results for: " . $term . "</h1>";

A normal visitor's search term ends up right where it should. But an attacker can send a link where the q parameter is set to instead of a search word. Because the value is echoed with no escaping, the browser parses that string as an actual

The fix: escape on output, based on context

The standard fix for text rendered into the body of an HTML page is htmlspecialchars() with the ENT_QUOTES flag and an explicit UTF-8 encoding. ENT_QUOTES matters because the default only escapes double quotes — without it, a value placed inside a single-quoted HTML attribute could still break out.

search.php (safe)

PHP
<?php
$term = $_GET['q'];
$safe = htmlspecialchars($term, ENT_QUOTES, 'UTF-8');
echo "<h1>Search results for: " . $safe . "</h1>";

With this fix, the same attack string is rendered as harmless, visible text — the browser shows the literal characters in the page instead of executing them, because the angle brackets have been converted to < and > entities.

Escaping changes depending on where the value lands

htmlspecialchars() is correct for text placed in the body of the page, between two tags. It is not automatically correct everywhere else, because HTML has several different "contexts," each with its own escaping rules.

  • HTML body context — text between tags, like <p>{value}</p> — use htmlspecialchars($value, ENT_QUOTES, 'UTF-8').

  • HTML attribute context — a value inside value="{value}" — still needs htmlspecialchars() with ENT_QUOTES, and the attribute itself must always be quoted, never left bare.

  • JavaScript context — a value being embedded inside an inline <script> block or an event handler — htmlspecialchars() alone is not sufficient here, because it does not escape characters that are dangerous inside a JS string, like a closing </script> tag. Use json_encode() to safely embed a PHP value as a JavaScript literal instead.

  • URL context — a value placed inside an href or src — use rawurlencode() on the value before concatenating it into the URL.

Passing a PHP value into an inline script safely

PHP
<?php
$username = $_SESSION['username'];
?>
<script>
  const currentUser = <?php echo json_encode($username); ?>;
</script>
Attribute values must always be quoted
htmlspecialchars() protects a quoted attribute like title="{value}", but if a developer omits the quotes entirely — title={value} — an attacker can inject a new attribute or event handler using a plain space, without needing a quote character at all. Always quote every HTML attribute, and treat an unquoted attribute as broken regardless of escaping.
Content-Security-Policy as a second layer

Escaping output correctly should already prevent XSS on its own, but a Content-Security-Policy (CSP) response header adds a second, independent layer of defense. A CSP tells the browser which sources of scripts, styles, and other resources are allowed to run on the page at all — for example, restricting scripts to your own domain and disallowing inline

  • Escape every piece of user-influenced data at the point it is output, not when it is received.

  • Use htmlspecialchars($value, ENT_QUOTES, 'UTF-8') for HTML body and attribute contexts.

  • Use json_encode() when embedding PHP data inside a <script> block, never string concatenation.

  • Quote every HTML attribute — an unquoted attribute is exploitable even with correct escaping elsewhere.

  • Add a Content-Security-Policy header as defense in depth, not as a replacement for output escaping.

Frameworks and template engines often escape by default
Modern template engines like Blade, Twig, or Next.js/React's JSX escape interpolated values automatically unless you deliberately opt out (`{!! !!}` in Blade, `| raw` in Twig, `dangerouslySetInnerHTML` in React). If you are writing plain PHP without a template engine, that automatic protection does not exist, so every `echo` involving user data needs the escaping call written explicitly.
Tip
Build a small helper function, such as `function e($value) { return htmlspecialchars($value, ENT_QUOTES, 'UTF-8'); }`, and use it for every piece of output derived from user input. A short, consistent habit like `<?php echo e($username); ?>` is far easier to apply correctly everywhere than remembering the full `htmlspecialchars()` call each time.