mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-26 00:52:18 +03:00
fix(relay): strip unsafe forwarded headers (#11533)
Validated in a combined 10-PR batch worktree off release/v3.8.51 tip. - Focused test: tests/unit/generated-relay-header-denylist.test.ts — 3/3 contract pass plus 30/30 related SSRF tests - typecheck:core, file-size, changelog-integrity, complexity, cognitive-complexity gates — all OK - Full-repo lint: 503 pre-existing problems confirmed identical on the pure release/v3.8.51 tip — unrelated to this diff ⚠️ base-red inherited: #11449 Thanks for applying one canonical header denylist across the generated Cloudflare/Vercel/Deno relays so hop-by-hop, framing, and proxy-auth/relay-control headers stop leaking upstream.
This commit is contained in:
1
changelog.d/fixes/generated-relay-header-sanitization.md
Normal file
1
changelog.d/fixes/generated-relay-header-sanitization.md
Normal file
@@ -0,0 +1 @@
|
||||
- Harden generated Cloudflare, Vercel, and Deno relays by stripping hop-by-hop, framing, and proxy authentication headers before forwarding requests upstream.
|
||||
@@ -105,7 +105,11 @@ Deno.serve(async (request) => {
|
||||
return new Response(resolved.reason, { status: resolved.status });
|
||||
}
|
||||
const headers = new Headers(request.headers);
|
||||
["x-relay-target", "x-relay-path", "x-relay-auth", "host"].forEach(h => headers.delete(h));
|
||||
[
|
||||
"host", "connection", "content-length", "keep-alive", "proxy-connection",
|
||||
"proxy-authenticate", "proxy-authorization", "transfer-encoding", "te", "trailer", "upgrade",
|
||||
"x-relay-target", "x-relay-path", "x-relay-auth",
|
||||
].forEach(h => headers.delete(h));
|
||||
const init = { method: request.method, headers };
|
||||
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||
init.body = request.body;
|
||||
|
||||
@@ -53,7 +53,11 @@ export default async function handler(req) {
|
||||
return new Response(resolved.reason, { status: resolved.status });
|
||||
}
|
||||
const headers = new Headers(req.headers);
|
||||
["x-relay-target", "x-relay-path", "x-relay-auth", "host"].forEach(h => headers.delete(h));
|
||||
[
|
||||
"host", "connection", "content-length", "keep-alive", "proxy-connection",
|
||||
"proxy-authenticate", "proxy-authorization", "transfer-encoding", "te", "trailer", "upgrade",
|
||||
"x-relay-target", "x-relay-path", "x-relay-auth",
|
||||
].forEach(h => headers.delete(h));
|
||||
const upstream = await fetch(resolved.url, {
|
||||
method: req.method,
|
||||
headers,
|
||||
|
||||
@@ -109,7 +109,11 @@ async function handleRelay(request) {
|
||||
}
|
||||
const relayPath = request.headers.get("x-relay-path") || "/";
|
||||
const headers = new Headers(request.headers);
|
||||
["x-relay-target", "x-relay-path", "x-relay-auth", "host"].forEach((h) => headers.delete(h));
|
||||
[
|
||||
"host", "connection", "content-length", "keep-alive", "proxy-connection",
|
||||
"proxy-authenticate", "proxy-authorization", "transfer-encoding", "te", "trailer", "upgrade",
|
||||
"x-relay-target", "x-relay-path", "x-relay-auth",
|
||||
].forEach((h) => headers.delete(h));
|
||||
const init = {
|
||||
method: request.method,
|
||||
headers,
|
||||
|
||||
45
tests/unit/generated-relay-header-denylist.test.ts
Normal file
45
tests/unit/generated-relay-header-denylist.test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
import { __buildRelayWorkerForTest } from "../../src/app/api/settings/proxy/deno-deploy/route";
|
||||
import { __buildRelayFunctionForTest } from "../../src/app/api/settings/proxy/vercel-deploy/route";
|
||||
import { buildCloudflareWorkerScript } from "../../src/lib/proxyRelay/cloudflareWorkerScript";
|
||||
|
||||
const FORBIDDEN_RELAY_HEADERS = [
|
||||
"host",
|
||||
"connection",
|
||||
"content-length",
|
||||
"keep-alive",
|
||||
"proxy-connection",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"transfer-encoding",
|
||||
"te",
|
||||
"trailer",
|
||||
"upgrade",
|
||||
"x-relay-target",
|
||||
"x-relay-path",
|
||||
"x-relay-auth",
|
||||
];
|
||||
|
||||
const relays = {
|
||||
Cloudflare: buildCloudflareWorkerScript("relay-secret"),
|
||||
Vercel: __buildRelayFunctionForTest("relay-secret"),
|
||||
Deno: __buildRelayWorkerForTest("relay-secret"),
|
||||
};
|
||||
|
||||
describe("generated relay request header sanitization", () => {
|
||||
for (const [runtime, source] of Object.entries(relays)) {
|
||||
it(`${runtime} strips relay controls, framing, and hop-by-hop headers`, () => {
|
||||
const deletionBlock = source.match(
|
||||
/const headers = new Headers\([^;]+\);\s*\[([\s\S]*?)\]\.forEach\(\(?h\)?\s*=>\s*headers\.delete\(h\)\);/
|
||||
);
|
||||
assert.ok(deletionBlock, `${runtime} generated source must sanitize forwarded headers`);
|
||||
|
||||
const deletedHeaders = [...deletionBlock[1].matchAll(/["']([^"']+)["']/g)].map(
|
||||
(match) => match[1]
|
||||
);
|
||||
assert.deepEqual(deletedHeaders, FORBIDDEN_RELAY_HEADERS);
|
||||
});
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user