Rook
AI red-teamer · 42nights

Reflected Cross-Site Scripting (XSS) in /search via unescaped 'q' parameter

medium6.1

xss · server.js:56-59

Introduced in 15b34d5cc6 by 42nights on 2026-06-03

Summary

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

$ curl -s 'http://127.0.0.1:4600/search?q=<script>ROOKPOC</script>'
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:N6.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, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;')
+      .replace(/"/g, '&quot;')
+      .replace(/'/g, '&#39;');
+  }
   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.