XSS — Cross-Site Scripting, htmlspecialchars, Content-Security-Policy
Concept
Cross-Site Scripting (XSS) allows attackers to inject JavaScript into web pages viewed by other users. The injected script runs with the victim's session — it can steal cookies, tokens, make API calls as the victim, or redirect to phishing pages.
Three types:
- Stored XSS: Malicious input saved to database and rendered to other users (comments, profiles, forum posts).
- Reflected XSS: Input from the URL/form immediately reflected in the response without storage (search queries, error messages).
- DOM-based XSS: JavaScript reads attacker-controlled data and writes it to the DOM without server-side involvement.
The fix: output encoding. When rendering user data in HTML, encode HTML entities: <script> becomes <script>. The browser renders the text, not the script.
htmlspecialchars(string $string, int $flags = ENT_QUOTES|ENT_SUBSTITUTE, string $encoding = 'UTF-8'): Converts <, >, &, ", ' to their HTML entity equivalents. Always pass ENT_QUOTES to also escape single quotes (important for attributes).
Context matters:
- HTML body:
htmlspecialchars($value). - HTML attribute:
htmlspecialchars($value, ENT_QUOTES). - JavaScript context: JSON-encode:
json_encode($value, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP). - URL context:
urlencode($value)orrawurlencode($value). - CSS context: Avoid dynamic CSS values; if unavoidable, allow only
[a-zA-Z0-9].
Laravel Blade: {{ $value }} automatically calls htmlspecialchars(). {!! $value !!} bypasses escaping — use only for trusted HTML (e.g., from a markdown renderer).
Code Example
<?php
declare(strict_types=1);
$userInput = '<script>document.cookie="stolen="+document.cookie</script>';
$username = 'O\'Brien <Alice>';
// VULNERABLE — raw output
echo "Hello, $userInput"; // script executes in browser!
// SAFE — HTML-encode output
echo "Hello, " . htmlspecialchars($userInput, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
// Output: Hello, <script>...</script> — safe text, not executed
// SAFE — attribute context
echo '<input value="' . htmlspecialchars($username, ENT_QUOTES) . '">';
// Output: <input value="O'Brien <Alice>">
// SAFE — URL context
echo '<a href="/profile?name=' . urlencode($username) . '">Profile</a>';
// SAFE — JavaScript context (embed PHP data in JS)
$data = ['name' => $username, 'role' => 'admin'];
echo '<script>var userData = ' . json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) . ';</script>';
// Blade templates — auto-escaped
// {{ $userComment }} → htmlspecialchars applied automatically
// {!! $userComment !!} → RAW — only use for trusted content like:
// {!! Markdown::parse($description) !!} // trusted rendered HTML
// Content Security Policy header — defense in depth
// Even if XSS occurs, CSP blocks inline script execution
header("Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'");
// Laravel middleware — set security headers
// Use: composer require bepsvpt/secure-headers
// Or set manually in AppServiceProvider::boot():
use Illuminate\Http\Response;
Response::macro('withSecurityHeaders', function() {
return $this
->header('X-Content-Type-Options', 'nosniff')
->header('X-Frame-Options', 'SAMEORIGIN')
->header('Referrer-Policy', 'strict-origin-when-cross-origin');
});