XSS Prevention

JA JASWANT Updated 19 Oct 2026
6 min read ·Lesson 119 of 143
In this post

What Is Cross-Site Scripting? In simple terms: Cross-site scripting (XSS) happens when an attacker manages to get their own JavaScript into your web page, so it runs in your visitors' browsers. From t

What Is Cross-Site Scripting?

In simple terms: Cross-site scripting (XSS) happens when an attacker manages to get their own JavaScript into your web page, so it runs in your visitors' browsers. From there, the script can do anything the user can: read the page, send requests in the user's name, show a fake login form, or redirect to a malicious site. It usually happens when user-supplied text is printed into HTML without escaping.

Technically: XSS is an injection vulnerability in the output of a web application. The browser cannot distinguish the application's own HTML and JavaScript from attacker-controlled content inserted into it. The primary defense is context-aware output encoding: every dynamic value is encoded for the exact place where it appears (HTML text, attribute, URL, JavaScript). A Content Security Policy (CSP) adds a second layer of protection.

Types of XSS

TypeHow the Payload ArrivesExample
ReflectedIn the current request, reflected into the responsesearch.php?q=<script>…</script> displayed as "Results for …"
StoredSaved in the database and shown to other usersA product review or profile bio containing a script
DOM-basedClient-side JavaScript inserts untrusted data into the pageelement.innerHTML = location.hash

Stored XSS is especially dangerous: every visitor of the affected page is attacked, including administrators.


Escaping for HTML: htmlspecialchars()

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

$comment = '<script>alert("hacked")</script>';
$name = '" autofocus onfocus="alert(1)';

echo "<p>" . e($comment) . "</p>\n";
echo '<input name="name" value="' . e($name) . '">' . "\n";

Output

<p><script>alert("hacked")</script></p>
<input name="name" value="" autofocus onfocus="alert(1)">

How It Works

CharacterBecomesWhy
< and >< and >Cannot start or end tags
&&Cannot start entities
""Cannot end double-quoted attributes
''Cannot end single-quoted attributes (ENT_QUOTES)

The browser displays the comment as text, and the name stays inside the value attribute instead of adding an event handler. ENT_SUBSTITUTE replaces invalid UTF-8 instead of returning an empty string. Always quote HTML attributes — escaping does not protect unquoted attributes (value=<?= e($x) ?>). See Escaping Output (htmlspecialchars) for details.


Different Contexts Need Different Encoding

ContextExampleCorrect Approach
HTML text<p>…</p>htmlspecialchars()
Quoted HTML attribute<input value="…">htmlspecialchars() with ENT_QUOTES
URL parameter<a href="/search?q=…">rawurlencode() or http_build_query(), then htmlspecialchars() for the attribute
Full URL in href/src<a href="…">Validate the scheme (http/https only), then htmlspecialchars()
JavaScript data<script>const data = …;</script>json_encode() with JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
CSS, event handlers (onclick), <script> code—Never insert untrusted data; use data attributes and external scripts instead

htmlspecialchars() does not stop <a href="javascript:alert(1)"> — the text contains no special characters. User-provided URLs (website fields in profiles, links in comments) must be validated:

<?php
function safeUrl(string $url): string
{
    $url = trim($url);

    if (preg_match('#^/(?![/\\\\])#', $url)) {
        return $url;   // a local path like /courses
    }

    $scheme = strtolower((string) parse_url($url, PHP_URL_SCHEME));

    return in_array($scheme, ['http', 'https'], true) ? $url : '#';
}

foreach (['https://gocourse.in/php', '/courses', 'javascript:alert(1)', 'JaVaScRiPt:alert(1)', '//evil.example'] as $url) {
    echo str_pad($url, 24) . '→ ' . safeUrl($url) . "\n";
}

Output

https://gocourse.in/php → https://gocourse.in/php
/courses                → /courses
javascript:alert(1)     → #
JaVaScRiPt:alert(1)     → #
//evil.example          → #

Only http, https, and local paths are allowed; everything else — javascript:, data:, vbscript:, protocol-relative URLs — is replaced. The result is still escaped with e() when printed into the attribute.

JavaScript: Pass Data as JSON

<?php
$productName = '</script><script>alert(1)</script>';

$json = json_encode($productName, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
echo "<script>const productName = $json;</script>\n";

Output

<script>const productName = "\u003C\/script\u003E\u003Cscript\u003Ealert(1)\u003C\/script\u003E";</script>

JSON_HEX_TAG encodes < and > as \u003C and \u003E, so the value cannot close the <script> element. JavaScript still sees the original string. An even cleaner pattern stores data in an attribute — <div id="app" data-product="<?= e(json_encode($product)) ?>"> — and reads it with JSON.parse(element.dataset.product).


User-Generated Rich Text

Comments, product descriptions, or blog posts sometimes need formatting (bold, links, lists). Escaping everything would remove the formatting; allowing raw HTML would allow scripts.

ApproachWhen
Plain text + nl2br(e($text))Most comments and messages
Markdown converted with a safe configuration (raw HTML disabled)Formatted content written by users
HTML sanitizer with an allow-list of tags and attributes (for example, HTML Purifier or symfony/html-sanitizer)Rich-text editors that produce HTML

Never try to remove "dangerous" tags with regular expressions or strip_tags() with allowed tags — attributes such as onerror and many encoding tricks get through.


DOM-Based XSS in JavaScript

The same rules apply in the browser. Data from fetch() responses, location, or localStorage must not be inserted as HTML:

// Vulnerable
results.innerHTML = `<li>${product.name}</li>`;

// Safe
const li = document.createElement('li');
li.textContent = product.name;
results.append(li);

Use textContent, setAttribute() (with validated URLs), and DOM methods; avoid innerHTML, outerHTML, insertAdjacentHTML(), document.write(), and eval() with untrusted data (see AJAX Live Search).


Defense in Depth: Content Security Policy

A Content Security Policy tells the browser which scripts may run. With a nonce-based policy, injected scripts without the correct nonce are blocked even if escaping was forgotten somewhere:

<?php
$nonce = base64_encode(random_bytes(16));

header("Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-$nonce'; object-src 'none'; base-uri 'self'");
?>
<script nonce="<?= $nonce ?>">
    // allowed: carries the nonce of this response
</script>
  • A new random nonce is generated for every response.
  • Inline scripts without the nonce, inline event handlers (onclick="…"), and scripts from other domains are blocked.
  • Start with Content-Security-Policy-Report-Only to find violations before enforcing the policy.

Additional layers:

MeasureEffect
HttpOnly session cookiesScripts cannot read the session cookie (see Secure Cookies)
Template engines with auto-escaping (Twig, Blade)Values are escaped by default; raw output must be requested explicitly
Input validationRejects unexpected data early (but is not a substitute for output encoding)

Common Mistakes

Escaping on Input Instead of Output

Effect: Data is double-escaped in some places and unescaped in others.

Fix: Store raw data; escape at the moment of output, for the context.

Forgetting Stored Data

Effect: Values from the database are trusted and printed unescaped.

Fix: Escape every dynamic value, regardless of its source.

Unquoted Attributes or URLs Without Scheme Checks

Effect: Escaping is bypassed.

Fix: Always quote attributes and validate URL schemes.

Using innerHTML With Data

Effect: DOM-based XSS.

Fix: Use textContent and DOM methods.


Best Practices

  • Escape every dynamic value for its output context; use a short e() helper or an auto-escaping template engine.
  • Validate user-provided URLs and allow only safe schemes.
  • Pass data to JavaScript with json_encode() and the JSON_HEX_* flags, or via data attributes.
  • Sanitize rich text with a proven allow-list sanitizer.
  • Add a nonce-based Content Security Policy and HttpOnly cookies as additional layers.

Official reference: https://www.php.net/manual/en/function.htmlspecialchars.php

0 Comments

Reviewed before they appear

No comments yet.

PHP
PHP Home PHP Switch PHP Match Alternative Syntax PHP Loops While Loop Do While Loop For Loop Foreach Loop Nested Loops Break Statement PHP Home PHP Intro PHP Install PHP Built-in Web Server PHP Syntax PHP Tags PHP and HTML PHP Comments PHP Multiline Comments PHP Variables Variables Scope PHP Casting Logical Operators Null Coalescing Operator Spaceship Operator Operator Precedence Continue Statement Modify Strings PHP If...Else...Elseif PHP If Modify Strings PHP If Operators PHP Functions Concatenate Strings PHP If...Else Slicing Strings User-defined Functions Slicing Strings PHP Shorthand if Escape Characters Default Argument Values PHP Nested if Pass by Reference String Interpolation Named Arguments PHP File Upload Variadic Functions Return Values Search in Strings Multibyte Strings Type Declarations in Functions PHP Numbers PHP Math PHP Floats PHP Random Numbers PHP Constants PHP Properties PHP Methods PHP Constructor Constructor Property Promotion PHP Cookies PHP Destructor PHP Access Modifiers Readonly Properties PHP Sessions PHP Inheritance Method Overriding PHP Filters The final Keyword CSRF Protection PHP Filters Advanced Session Security Secure Cookies PHP Magic Constants File Upload Security PHP Callback Functions PHP Operators Filesystem Security PHP JSON Arithmetic Operators Error Reporting in Production Hiding PHP Assignment Operators PHP Configuration (php.ini) Comparison Operators PHP Command Line (CLI) PHP Exceptions Increment / Decrement Operators Migrating to PHP 8 PHP Form Validation PHP Form Required PHP Form URL/E-mail Error Handling Form Input Sanitizing Escaping Output (htmlspecialchars) Custom Error Handler Checkbox, Radio and Select Inputs Sticky Forms PHP Form Complete PHP Date and Time Date Formatting Error Logging HTTP Headers and Redirects Output Buffering Sending Email PHP cURL Consuming REST APIs PHP Generators AJAX PHP AJAX Database AJAX XML AJAX Live Search AJAX Poll AJAX with JSON Security Introduction Password Hashing SQL Injection Prevention XSS Prevention Garbage Collection Project: Simple Blog PHP What is OOP PHP Classes/Objects The $this Keyword Access Array Items Update Array Items Add Array Items Remove Array Items Sorting Arrays Multidimensional Arrays Array Destructuring Array Spread Operator Search in Arrays Merge Arrays PHP Abstract Classes PHP Interfaces Polymorphism PHP Traits PHP Static Methods PHP Static Properties Late Static Binding Magic Methods Object Cloning
Ask about this post
AI Ask about this post

Ask questions about XSS Prevention and get answers drawn from it.

Signed-in readers only.