From be6950a1d72640c5a43abb766adcfc71291b4665 Mon Sep 17 00:00:00 2001 From: Patrick Date: Mon, 18 May 2026 16:05:02 -0700 Subject: [PATCH] fix(sast): User-controlled `url` query parameter is fetched server-side and response body is reflected, enablin The handler concatenated the user-controlled `req.query.url` with `req.query.symbol` and passed the result to `needle.get`, then reflected the response body to the client. An attacker could point `url` at internal services such as the cloud instance metadata endpoint (e.g. `http://169.254.169.254/latest/meta-data/`) and receive the response, leaking credentials and enabling SSRF. The fix introduces a small allow-list of permitted upstream research providers and rejects any request whose `url` p --- app/routes/research.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/routes/research.js b/app/routes/research.js index c3ae59df6..a47f7f758 100644 --- a/app/routes/research.js +++ b/app/routes/research.js @@ -9,10 +9,27 @@ function ResearchHandler(db) { const researchDAO = new ResearchDAO(db); + // Allow-list of permitted upstream research providers. Only requests whose + // base URL exactly matches one of these origins are allowed, to prevent + // SSRF (e.g. fetching cloud metadata endpoints like 169.254.169.254). + const ALLOWED_RESEARCH_URLS = [ + "https://www.google.com/finance?q=", + "https://finance.yahoo.com/quote/" + ]; + this.displayResearch = (req, res) => { if (req.query.symbol) { - const url = req.query.url + req.query.symbol; + const baseUrl = req.query.url; + if (!ALLOWED_RESEARCH_URLS.includes(baseUrl)) { + res.writeHead(400, { + "Content-Type": "text/html" + }); + res.write("

Invalid research URL.

"); + return res.end(); + } + const symbol = encodeURIComponent(req.query.symbol); + const url = baseUrl + symbol; return needle.get(url, (error, newResponse, body) => { if (!error && newResponse.statusCode === 200) { res.writeHead(200, {