Files
diegosouzapw--omniroute/tests/unit/vercel-deploy-relay-path-ssrf.test.ts
2026-07-13 13:39:12 +08:00

88 lines
3.5 KiB
TypeScript

// SSRF regression guard for the Vercel relay worker (lockstep with the Deno
// Deploy relay fix; PR #4643 / deno-deploy follow-up).
//
// The generated Vercel edge function reads an attacker-controlled `x-relay-path`
// header and previously appended it to the (validated) `x-relay-target` origin
// via string concatenation: `fetch(target.replace(/\/$/, "") + relayPath)`.
// That is bypassable via userinfo (`/x@evil.com`), a backslash (`\evil.com`), or
// a protocol-relative path (`//evil.com/x`) — the same hole the Deno worker had.
//
// The fix reuses the SAME pure `resolveRelayTarget()` guard (exported from the
// deno-deploy route, embedded into both workers via Function#toString). This
// test mirrors `deno-deploy-relay-path-ssrf.test.ts`: it asserts the generated
// Vercel worker no longer string-concatenates the path and embeds the guard.
// The pure-function behavior itself is covered once in the deno test (same
// function); here we focus on the Vercel worker wiring.
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { resolveRelayTarget } from "../../src/app/api/settings/proxy/deno-deploy/route";
import { __buildRelayFunctionForTest } from "../../src/app/api/settings/proxy/vercel-deploy/route";
const TARGET = "https://api.anthropic.com";
describe("vercel relay reuses the SSRF-safe resolveRelayTarget guard", () => {
it("accepts a legitimate absolute path", () => {
const r = resolveRelayTarget(TARGET, "/v1/foo");
assert.equal(r.ok, true);
if (r.ok) assert.equal(r.url, "https://api.anthropic.com/v1/foo");
});
it("rejects //evil.com/x (host swap)", () => {
const r = resolveRelayTarget(TARGET, "//evil.com/x");
assert.equal(r.ok, false);
if (!r.ok) assert.equal(r.status, 403);
});
it("rejects /x@evil.com (userinfo)", () => {
const r = resolveRelayTarget(TARGET, "/x@evil.com");
assert.equal(r.ok, false);
if (!r.ok) assert.equal(r.status, 403);
});
it("rejects \\evil.com (backslash)", () => {
const r = resolveRelayTarget(TARGET, "\\evil.com");
assert.equal(r.ok, false);
if (!r.ok) assert.equal(r.status, 403);
});
it("rejects a value without a leading '/'", () => {
const r = resolveRelayTarget(TARGET, "evil.com/x");
assert.equal(r.ok, false);
if (!r.ok) assert.equal(r.status, 403);
});
});
describe("buildRelayFunction — generated Vercel worker has no string-concat SSRF hole", () => {
it("does not append relayPath to target by string concatenation", () => {
const worker = __buildRelayFunctionForTest("deadbeefcafe");
assert.ok(
!worker.includes("+ relayPath"),
"Vercel worker must not contain `+ relayPath` string concatenation"
);
assert.ok(
!/\.replace\(\s*\/\\?\/\$\/\s*,\s*["']{2}\s*\)\s*\+\s*relayPath/.test(worker),
"Vercel worker must not append relayPath to target by string concatenation"
);
});
it("embeds the resolveRelayTarget guard and fetches the resolved url", () => {
const worker = __buildRelayFunctionForTest("deadbeefcafe");
assert.ok(
worker.includes("resolveRelayTarget"),
"the Vercel worker must call the shared resolveRelayTarget guard"
);
assert.ok(
worker.includes("resolved.url"),
"the Vercel worker must fetch the SSRF-validated resolved url"
);
});
it("still ships the edge runtime config marker", () => {
const worker = __buildRelayFunctionForTest("deadbeefcafe");
assert.ok(
worker.includes('runtime: "edge"'),
"the Vercel edge runtime config must be preserved"
);
});
});