Reflected Cross-Site Scripting (XSS) in /search via unescaped 'q' parameter
xss · server.js:56-59
15b34d5cc6 by 42nights on 2026-06-03Summary
The /search handler in server.js takes the user-controlled 'q' query string parameter and interpolates it directly into an HTML response using a template literal, with no HTML encoding or sanitization. A request such as /search?q=<script>ROOKPOC</script> is echoed back verbatim inside a <div>, and the injected <script> tag is parsed and executed by the browser. The endpoint should HTML-escape any reflected user input (or use a templating engine that escapes by default) before placing it into the response body.
Impact. An attacker can craft a malicious link (for example, sent via email, chat, or hosted on another site) that, when clicked by a logged-in user, runs attacker-controlled JavaScript in the victim's browser under the application's origin. That script can steal session cookies and authentication tokens, read or modify any data the user can see, perform actions on the user's behalf (account takeover, password change, fund transfer, etc.), and pivot to phishing by rewriting the page. Because no special privileges are needed and the payload travels in a normal URL, this is trivially weaponizable against any authenticated user.
Vulnerable code — server.js
🔴 if (parsed.pathname === "/search") {🔴 const term = q.q || "";🔴 send(res, 200, `<html><body><div>You searched: ${term}</div></body></html>`, "text/html");🔴 return;🔴 }
Working exploit
curl -s 'http://127.0.0.1:4600/search?q=<script>ROOKPOC</script>'
Exploit transcript
HTTP 200 Content-Type: text/html <html><body><div>You searched: <script>ROOKPOC</script></div></body></html>
Confirmed: reflected XSS confirmed — the injected payload reflected unescaped in a text/html response
CVSS v3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N→ 6.1 (medium)Recommended fix
AI-suggested fix — review before applying (derived from analysis of untrusted repo content):
Escape user input before placing it in HTML. Example patch:
--- a/server.js
+++ b/server.js
@@
+ function escapeHtml(s) {
+ return String(s)
+ .replace(/&/g, '&')
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ }
if (parsed.pathname === "/search") {
- const term = q.q || "";
- send(res, 200, `<html><body><div>You searched: ${term}</div></body></html>`, "text/html");
+ const term = escapeHtml(q.q || "");
+ send(res, 200, `<html><body><div>You searched: ${term}</div></body></html>`, "text/html");
return;
}
Also consider sending a strict Content-Security-Policy header (e.g. "default-src 'self'; script-src 'self'") as defense in depth.Ship the fix
Rook found it and proved it. Hand the finding — with its working exploit as a failing test — to Otis to write the fix PR.