Skip to content
Open
Show file tree
Hide file tree
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
2 changes: 2 additions & 0 deletions main.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import express from "express";
import { spawn } from "child_process";
import edgePreviewRouter from "./src/routes/edge-preview";

const app = express();
app.use(express.json());
Comment thread
vidoc-local[bot] marked this conversation as resolved.
Comment thread
vidoc-local[bot] marked this conversation as resolved.
app.use(edgePreviewRouter);

app.get("/", (req, res) => {
res.send("Hello, World!");
Expand Down
18 changes: 18 additions & 0 deletions src/routes/edge-preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Router } from "express";

const router = Router();

// The edge gateway strips both headers from callers, injects them only after
// policy checks, and prevents direct origin access. The repository intentionally
// has no gateway manifest because that policy is managed by the platform team.
router.get("/internal/edge-preview", async (req, res) => {
if (req.header("x-edge-attested") !== "1") {
return res.status(403).send("forbidden");
}

const previewUrl = String(req.header("x-internal-preview-url") ?? "");
const response = await fetch(previewUrl);
res.status(response.status).send(await response.text());
Comment on lines +12 to +15

@vidoc-local vidoc-local Bot Jul 29, 2026

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

Unvalidated header value is used to fetch arbitrary URLs, enabling SSRF. Fix by validating and whitelisting URLs and attaching proper access controls before performing the fetch.

Explanation

The code takes a value from x-internal-preview-url and immediately uses it as the destination of fetch. If an attacker can set this header and pass x-edge-attested: 1, the server will fetch any external or internal URL and relay the response back. The problem is not with fetch itself but with trusting client-supplied data and not enforcing any allowed targets or network boundaries. The fix is to ensure only trusted URLs are allowed and that only authenticated/authorized requests can trigger the fetch.

Example issue in code (simplified):

const previewUrl = String(req.header("x-internal-preview-url") ?? "");
const response = await fetch(previewUrl);

Potential impact: attacker can probe internal services or exfiltrate data via the server.

Fix approach (non-security jargon): validate the header against a whitelist of allowed domains or IP ranges, enforce a safe default, and require proper authorization before proceeding.

Steps to fix (high level):

  • Add a list of allowed base URLs (and/or allowed hostnames).
  • Normalize and parse the URL from the header.
  • If the URL is not in the allowed list, return 400 or 403.
  • Perform the fetch only after the URL passes the checks.
  • Keep existing edge-attested check or move it to a stricter access control that runs before any fetch.
  • Return sanitized results or limit the data exposed (e.g., only status, or a small subset of body).

This keeps the route usable for legitimate previews while preventing arbitrary outgoing requests from the server.

Debug { "id": "019fadfa-971b-71df-8de7-ebbc6e2a041b", "codebaseId": "019facb5-bc3d-7323-a4d5-e405e2f73e68", "path": "src/routes/edge-preview.ts", "rangeStart": 7, "rangeEnd": 15, "line": 12, "signature": "019fadfa-814e-762b-a8f7-7264d9c0debb" }
Possible fix - diff
*** Begin Patch
*** Update File: src/routes/edge-preview.ts
@@
-router.get("/internal/edge-preview", async (req, res) => {
-  if (req.header("x-edge-attested") !== "1") {
-    return res.status(403).send("forbidden");
-  }
-
-  const previewUrl = String(req.header("x-internal-preview-url") ?? "");
-  const response = await fetch(previewUrl);
-  res.status(response.status).send(await response.text());
-});
+router.get("/internal/edge-preview", async (req, res) => {
+  // Require the edge attestation header
+  if (req.header("x-edge-attested") !== "1") {
+    return res.status(403).send("forbidden");
+  }
+
+  // Validate and whitelist the target URL before fetching
+  const rawUrl = String(req.header("x-internal-preview-url") ?? "").trim();
+  if (!rawUrl) {
+    return res.status(400).send("bad request: missing x-internal-preview-url");
+  }
+
+  let url: URL;
+  try {
+    url = new URL(rawUrl);
+  } catch {
+    return res.status(400).send("bad request: invalid URL");
+  }
+
+  // Whitelist: only allow certain hosts/domains
+  const ALLOWED_HOSTS = [
+    // add trusted internal/internal cloud endpoints here
+    "example-internal.local",
+    "metadata.internal",
+  ];
+
+  if (!ALLOWED_HOSTS.includes(url.host)) {
+    return res.status(403).send("forbidden: host not allowed");
+  }
+
+  // Optional: enforce allowed protocols only
+  if (url.protocol !== "http:" && url.protocol !== "https:") {
+    return res.status(400).send("bad request: unsupported protocol");
+  }
+
+  try {
+    const response = await fetch(url.toString());
+    // Limit the amount of data sent back to the client if needed, e.g., stream or truncated body
+    const body = await response.text();
+    res.status(response.status).send(body);
+  } catch (err) {
+    res.status(502).send("bad gateway: failed to fetch target");
+  }
+});
*** 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!

});

export default router;