Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ app.get("/", (req, res) => {
res.send("Hello, World!");
});

// Deployment configuration controls route activation, direct-origin access,
// and whether the edge strips and reinjects this trusted workload header.
app.post("/internal/image-proxy", async (req, res) => {
if (process.env.IMAGE_PROXY_ENABLED !== "true") {
return res.status(404).send("not found");
}
if (req.header("x-trusted-workload") !== "image-service") {
return res.status(403).send("forbidden");
}

const imageUrl = String(req.body?.imageUrl ?? "");
const upstream = await fetch(imageUrl);
Comment thread
vidoc-local[bot] marked this conversation as resolved.
Comment on lines +13 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Potential issue · High severity

The /internal/image-proxy endpoint trusts a client header and a user-provided URL, allowing an attacker to make the server fetch internal resources (SSRF) and leak data.

Explanation

The code directly takes imageUrl from the request body and fetches it without validating where it points. If someone can set x-trusted-workload to image-service (or bypass edge protections), they can cause the server to request internal or sensitive endpoints and return the response to the attacker. There are no checks to restrict which URLs can be fetched, and the header-based gate is not a reliable defense inside the application. This leads to potential data leakage and access to internal services.

Example flow: attacker sends POST /internal/image-proxy with body { imageUrl: "http://internal-service.local/secret" } and header x-trusted-workload: image-service. The server fetches that URL and returns the raw response content to the attacker.

Fix should ensure the endpoint cannot fetch arbitrary URLs and does not rely on spoofable headers for authorization.

Debug { "id": "019fb36a-2727-728e-b6a6-786aa98e022a", "codebaseId": "019facb5-bc3d-7323-a4d5-e405e2f73e68", "path": "main.ts", "rangeStart": 12, "rangeEnd": 22, "line": 21, "signature": "019fb35d-7ad6-7409-a6dc-cbcfb8ddf73b" }
Possible fix - diff
*** Begin Patch
*** Update File: main.ts
@@
-app.post("/internal/image-proxy", async (req, res) => {
-  if (process.env.IMAGE_PROXY_ENABLED !== "true") {
-    return res.status(404).send("not found");
-  }
-  if (req.header("x-trusted-workload") !== "image-service") {
-    return res.status(403).send("forbidden");
-  }
-
-  const imageUrl = String(req.body?.imageUrl ?? "");
-  const upstream = await fetch(imageUrl);
-  res.status(upstream.status).send(await upstream.text());
-});
+app.post("/internal/image-proxy", async (req, res) => {
+  // Feature flag check remains
+  if (process.env.IMAGE_PROXY_ENABLED !== "true") {
+    return res.status(404).send("not found");
+  }
+
+  // Do not rely on headers for access control. If this route is meant to be internal,
+  // gate it at the edge or remove this route from public exposure entirely.
+  // In-code check is intentionally removed to prevent header spoofing from granting access.
+
+  // Validate input strictly. Do not allow arbitrary URLs. Only allow a fixed set of trusted endpoints
+  // or a very controlled internal proxy behavior. Here we implement a strict allowlist approach.
+  const imageUrl = String(req.body?.imageUrl ?? "").trim();
+
+  // Simple allowlist: only allow internal hostnames that start with "http://internal" or "https://internal".
+  // This should be replaced with your actual internal allowlist, but must not accept arbitrary URLs.
+  const isAllowed = /^(https?:\/\/)(internal[.-][^/]+|127|10|192)\S*/.test(imageUrl);
+  if (!isAllowed) {
+    return res.status(400).send("invalid imageUrl");
+  }
+
+  try {
+    const upstream = await fetch(imageUrl);
+    const text = await upstream.text();
+    res.status(upstream.status).send(text);
+  } catch (err) {
+    res.status(502).send("bad gateway");
+  }
+});
*** End Patch

📋 Export to AI agent


Did we do a good job? 👍 Was helpful, 👎 Needs improvement
Questions about this finding? Tag @vidoc-local in a reply and I'll answer. Specific feedback or suggestions are welcome too!

res.status(upstream.status).send(await upstream.text());
});

const PORT = process.env.PORT || 5000;
app.listen(PORT, "0.0.0.0", () => console.log(`listening on ${PORT}`));

Expand Down