security

Cross-Site Scripting (XSS)

Cross-site scripting occurs when attacker-controlled content is executed as code in another user's browser. Stored, reflected, and DOM-based XSS differ in where the unsafe value comes from, but all require careful output handling.

What Matters

  • Escape untrusted values for the exact HTML, attribute, URL, or JavaScript context.
  • Avoid inserting untrusted text into inline JavaScript or event-handler attributes.
  • Use a maintained HTML sanitiser only when users are intentionally allowed to submit markup.
  • Add a Content Security Policy as defence in depth, not as a replacement for escaping.
  • Review frontend DOM updates such as innerHTML, not only server-rendered templates.

Practical Example

PHP example
<?php

declare(strict_types=1);

function safeComment(string $comment): string
{
    return htmlspecialchars($comment, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

echo safeComment('<img src=x onerror=alert(1)>') . PHP_EOL;

// Prints:
// &lt;img src=x onerror=alert(1)&gt;

In Application Work

Stored comments, rich-text editors, search pages, admin dashboards, and email previews are frequent XSS review areas. Remember that an admin user is still a valuable XSS target.

Three Common XSS Shapes

Reflected XSS appears immediately in a response, such as a search page rendering an unsafe query term. Stored XSS is saved first, such as a comment later rendered in an admin screen. DOM-based XSS happens when browser JavaScript inserts unsafe data into the page.

reflected: request value -> unsafe HTML response
stored:    saved value -> later unsafe HTML response
DOM-based: browser value -> unsafe DOM update

The source may differ, but the review question is the same: where does data become executable browser content?

Rich Text Needs Sanitisation

Escaping is correct when markup should display as text. If users are intentionally allowed to submit formatting such as links or emphasis, use a maintained HTML sanitiser with a narrow allow-list. Do not build a sanitiser from regular expressions.

Review Browser-Side Sinks

Server-side escaping is not enough when JavaScript later places a value into innerHTML, outerHTML, or a similar API. Prefer text APIs such as textContent unless HTML is genuinely required and sanitised.

Content Security Policy is useful defence in depth. It does not make unsafe rendering acceptable.

What To Check

Before moving on, make sure you can:

  • describe reflected, stored, and DOM-based XSS;
  • escape plain text at the rendering boundary;
  • recognise dangerous inline-script and DOM contexts;
  • use sanitisation only for intentionally allowed markup;
  • distinguish reflected, stored, and DOM-based review paths.

Practice

Practice: Render Search Results Safely

Render a search term and result title without allowing HTML execution.

Requirements

  • Escape both values for HTML text.
  • Do not remove angle brackets with ad hoc string replacements.
  • Show a malicious search term being rendered as text.
Show solution

Escaping preserves the text while preventing it from becoming markup.

PHP example
<?php

declare(strict_types=1);

function html(string $value): string
{
    return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
}

function resultHeading(string $term, string $title): string
{
    return '<h2>Results for ' . html($term) . '</h2><p>' . html($title) . '</p>';
}

echo resultHeading('<script>alert(1)</script>', 'Desk <Lamp>') . PHP_EOL;

// Prints:
// <h2>Results for &lt;script&gt;alert(1)&lt;/script&gt;</h2><p>Desk &lt;Lamp&gt;</p>

The same rule applies to values loaded from a database: stored data may still contain attacker-controlled text.