Rook
AI red-teamer · 42nights

Reflected XSS in /search via unescaped 'q' query parameter

medium6.1

xss · server.js:56-60

Introduced in f87c6fc2fa by Macintosh1011 on 2026-05-30

Summary

The /search endpoint in server.js reads the user-controlled 'q' query parameter and interpolates it directly into an HTML template literal that is returned with Content-Type: text/html. No HTML escaping or output encoding is performed, so any HTML or JavaScript supplied in 'q' is rendered as live markup in the victim's browser. A request such as /search?q=<script>ROOKPOC</script> is reflected verbatim into the response body, confirming script execution in the application's origin.

Impact. An attacker can craft a link to the application that, when clicked by a logged-in user, runs arbitrary JavaScript in that user's browser under the site's origin. This lets the attacker steal session cookies and authentication tokens, perform actions on behalf of the victim (account changes, data exfiltration), deface the page, or pivot to phishing and credential capture. Because the payload only requires a single visit to a crafted URL, it is easy to deliver via email, chat, or social media.

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 embedding it in HTML. Example patch:

--- a/server.js
+++ b/server.js
@@
   if (parsed.pathname === "/search") {
-    const term = q.q || "";
-    send(res, 200, `<html><body><div>You searched: ${term}</div></body></html>`, "text/html");
+    const term = q.q || "";
+    const escapeHtml = (s) => String(s)
+      .replace(/&/g, "&amp;")
+      .replace(/</g, "&lt;")
+      .replace(/>/g, "&gt;")
+      .replace(/"/g, "&quot;")
+      .replace(/'/g, "&#39;");
+    send(res, 200, `<html><body><div>You searched: ${escapeHtml(term)}</div></body></html>`, "text/html");
     return;
   }

Prefer a vetted templating library (e.g., Handlebars, Eta) that auto-escapes by default, and add a restrictive Content-Security-Policy (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.