flawopen.com/Cross-Site Scripting/PHP

Cross-Site Scripting in PHP

High CWE-79 Draft — pending review
ELI5

Imagine a guestbook where visitors write a public note. Stored XSS is like someone writing a note that isn't just text — it's a hidden trick that makes the guestbook page itself start doing things, like stealing the next visitor's login session, the moment anyone opens the page to read it.

Key terms on this page
output encoding
Converting characters that have special meaning in HTML (like < and >) into harmless equivalents before inserting untrusted text into a page, so the browser displays it as text instead of running it as markup or script.
DOM
The in-memory tree structure a browser builds from a page's HTML — where an element's innerHTML is set determines whether inserted content is rendered as inert text or executable markup.

What's happening

Cross-site scripting happens when untrusted input reaches the rendered page without proper output encoding. PHP has no auto-escaping at all for a plain echo — unlike a templating engine, safety is entirely manual unless you use one that escapes by default (like Twig).

Real-world impact

In 2005, an 19-year-old user exploited a stored XSS flaw on MySpace to create the "Samy" worm — a script that added itself to every profile that viewed it, infecting over a million profiles within about 20 hours and forcing MySpace offline to contain it. It remains one of the most-cited demonstrations of how quickly stored XSS can self-propagate.

Source: widely documented in security industry retrospectives — see References below.

Vulnerable vs. fixed

VULNERABLE
// $_GET value echoed with no encoding
echo "<div>Welcome, "
   . $_GET['name'] . "</div>";
FIXED
// value HTML-encoded before output
echo "<div>Welcome, " . htmlspecialchars(
  $_GET['name'], ENT_QUOTES, 'UTF-8'
) . "</div>";

Why the fix works

htmlspecialchars() converts <, >, &, and quote characters into their HTML entity equivalents before the string reaches the response — the browser displays them as visible text instead of parsing them as markup. Plain echo performs no such conversion; it writes exactly what it's given.

PHP-specific gotchas

ENT_QUOTES matters — the default misses single quotes

Without ENT_QUOTES, htmlspecialchars() only encodes double quotes by default in older PHP versions, leaving single-quote-delimited HTML attributes still breakable. Always pass it explicitly.

PHP has no template auto-escaping unless you opt into one

Unlike Django, Rails, or Flask/Jinja2, plain PHP templates (raw .php files mixing HTML and <?= ?>) provide zero default protection — every single output point needs its own htmlspecialchars() call, or a templating layer like Twig (which escapes by default) needs to be adopted.

Context matters: HTML-attribute vs. JavaScript vs. URL

htmlspecialchars() is correct for HTML body and attribute context. A value reflected into an inline <script> block or a href needs different, context-specific encoding — one function doesn't cover every output context.

Common misconceptions

"I sanitize input when it's saved, so output is safe"

Input sanitization and output encoding are different defenses for different purposes — sanitizing on the way in doesn't guarantee every output context downstream is safe, especially if the same field is rendered in HTML, an attribute, and a JS context in different places.

"Twig protects me automatically"

True for standard {{ }} output — false for the explicit |raw filter, Twig's own named escape hatch.

How to check if you're affected

grep -rn "echo.*\$_\(GET\|POST\|REQUEST\)" --include="*.php" . | grep -v htmlspecialchars grep -rn "|raw" --include="*.twig" .
Static analyzers like Psalm or PHPStan with taint-analysis plugins can trace tainted request data into an unescaped echo automatically.

Prevention checklist

FAQ

Is this the same bug as SQL injection?

Same underlying shape — untrusted data mixed into a command's structure without encoding — but the target and damage differ: SQL injection targets the database, XSS targets other users' browsers.

Does Laravel's Blade protect me automatically?

Yes for its standard {{ }} syntax, which escapes by default. Blade's {!! !!} is its explicit unescaped-output syntax — the same risk as |raw in Twig if used on untrusted data.

References

View in: Python JavaScript Go Java PHP C# Ruby C/C++ Rust Kotlin Swift Solidity (N/A)