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
search.php (vulnerable)
<?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 $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>— usehtmlspecialchars($value, ENT_QUOTES, 'UTF-8').HTML attribute context— a value insidevalue="{value}"— still needshtmlspecialchars()withENT_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. Usejson_encode()to safely embed a PHP value as a JavaScript literal instead.URL context— a value placed inside anhreforsrc— userawurlencode()on the value before concatenating it into the URL.
Passing a PHP value into an inline script safely
<?php $username = $_SESSION['username']; ?> <script> const currentUser = <?php echo json_encode($username); ?>; </script>
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.