XSS and Escaping

Cross-Site Scripting and Contextual Output Escaping

Cross-site scripting (XSS) is injection into a page instead of a query: a user-controlled value ends up in HTML the browser runs as script. Reflected XSS bounces back from the request; stored XSS is written to the database and fires for everyone who views the page. Both have one cure: escape the value for HTML as you print it. The endpoints below echo input verbatim, then escape it when mode=safe.

Two XSS endpoints; the un-escaped branches are for learning onlyPHP
$q = $_GET['q'] ?? '';                                       // reflected: echoes the query string
if (($_GET['mode'] ?? '') === 'safe') $q = htmlspecialchars($q, ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8');
echo "<p>No results for: $q</p>";
$b = $safe ? htmlspecialchars($row['body'], ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8') : $row['body'];
echo "<li>$b</li>\n";                        // stored: review printed raw or escaped

Sending a <script> payload to the reflected endpoint (vulnerable, then mode=safe), then posting an <img onerror> review and rendering it both ways, produces:

Output of 177
<p>No results for: <script>alert(document.cookie)</script></p>
<p>No results for: &lt;script&gt;alert(document.cookie)&lt;/script&gt;</p>
<li><img src=x onerror=alert(1)></li>
...
<li>&lt;img src=x onerror=alert(1)&gt;</li>
...

The vulnerable reflected page returned a live <script> that could read document.cookie; the stored page saved an <img onerror> that fires for every visitor. The review was stored with a prepared statement, so the flaw is purely at output. Escaping turned each payload into text (< became &lt;). Escape on output, not input (the next subsection). Two further layers help: a CSP header (Content Security Policy) that stops inline script if a payload slips through, and the HttpOnly cookie flag (Cookie Attributes) that keeps JavaScript from the session ID.