Files
OmniRoute/tests/unit/authz/public-origin.test.ts
Arthur Bodera 1c18be4f8f fix: centralize public origin checks for proxied dashboards (#5278)
Centralizes browser-mutation origin validation into `src/server/origin/publicOrigin.ts` and wires it through the authz pipeline, replacing the per-route same-origin-only check that 403'd dashboard mutations when served behind a reverse proxy on a different public origin. The new module resolves the allowed public origin from configured base-URL env vars or trusted forwarded headers (only when OMNIROUTE_TRUST_PROXY is set AND the peer is loopback/LAN via peer-stamp), validates Sec-Fetch-Site metadata, and sanitizes Host/Forwarded inputs (rejects control chars, userinfo, path/query in Host).

Reviewed sound; validated locally: authz/public-origin + pipeline suites 27/27 green (incl. invalid-origin reject + configured-origin accept), typecheck clean. Maintainer fix-up: moved the new test from tests/unit/server/ (not collected by any runner — orphan-test gate fail) into tests/unit/authz/. Remaining red CI shards are the pre-existing #4076 Dockerfile heap base-red on `main` (unrelated; de-brittled in the v3.8.40 release line).

Co-authored-by: Thinkscape <Thinkscape@users.noreply.github.com>
2026-06-29 01:43:14 -03:00

222 lines
7.6 KiB
TypeScript

import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { describe, it, beforeEach, after } from "node:test";
import { PEER_IP_HEADER } from "@/server/authz/headers";
import {
getPublicOriginCandidates,
resolvePublicOrigin,
trustsForwardedHeaders,
validateBrowserMutationOrigin,
} from "@/server/origin/publicOrigin";
const ORIGINAL_ENV = {
OMNIROUTE_PUBLIC_BASE_URL: process.env.OMNIROUTE_PUBLIC_BASE_URL,
NEXT_PUBLIC_BASE_URL: process.env.NEXT_PUBLIC_BASE_URL,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
OMNIROUTE_TRUST_PROXY: process.env.OMNIROUTE_TRUST_PROXY,
OMNIROUTE_PEER_STAMP_TOKEN: process.env.OMNIROUTE_PEER_STAMP_TOKEN,
};
function restoreEnv() {
for (const [key, value] of Object.entries(ORIGINAL_ENV)) {
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
}
function clearPublicOriginEnv() {
delete process.env.OMNIROUTE_PUBLIC_BASE_URL;
delete process.env.NEXT_PUBLIC_BASE_URL;
delete process.env.NEXT_PUBLIC_APP_URL;
delete process.env.OMNIROUTE_TRUST_PROXY;
delete process.env.OMNIROUTE_PEER_STAMP_TOKEN;
}
function stampedPeer(ip: string): Record<string, string> {
const token = randomUUID();
process.env.OMNIROUTE_PEER_STAMP_TOKEN = token;
return { [PEER_IP_HEADER]: `${token}|${ip}` };
}
beforeEach(() => {
clearPublicOriginEnv();
});
after(() => {
restoreEnv();
});
describe("public origin resolution", () => {
it("uses configured public base URLs before the internal request URL", () => {
process.env.NEXT_PUBLIC_BASE_URL = "https://gateway.example.test/app/";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions");
assert.deepEqual(resolvePublicOrigin(request), {
origin: "https://gateway.example.test",
source: "configured",
});
assert.equal(
validateBrowserMutationOrigin(
new Request(request.url, {
method: "POST",
headers: { origin: "https://gateway.example.test" },
})
).ok,
true
);
});
it("preserves configured source priority when it equals the request URL", () => {
process.env.NEXT_PUBLIC_BASE_URL = "http://omniroute:20128/app";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions");
assert.deepEqual(resolvePublicOrigin(request), {
origin: "http://omniroute:20128",
source: "configured",
});
});
it("accepts all configured public origins while resolving the highest-priority one", () => {
process.env.OMNIROUTE_PUBLIC_BASE_URL = "https://assets.example.test/images";
process.env.NEXT_PUBLIC_BASE_URL = "https://gateway.example.test/app";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions", {
headers: { origin: "https://gateway.example.test" },
});
assert.deepEqual(resolvePublicOrigin(request), {
origin: "https://assets.example.test",
source: "configured",
});
assert.deepEqual(
getPublicOriginCandidates(request).filter((candidate) => candidate.source === "configured"),
[
{ origin: "https://assets.example.test", source: "configured" },
{ origin: "https://gateway.example.test", source: "configured" },
]
);
assert.equal(validateBrowserMutationOrigin(request).ok, true);
});
it("keeps the internal request URL as an accepted candidate", () => {
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions");
assert.deepEqual(getPublicOriginCandidates(request), [
{ origin: "http://omniroute:20128", source: "request-url" },
]);
});
it("does not trust spoofed forwarded headers by default", () => {
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions", {
headers: {
origin: "https://attacker.example",
"x-forwarded-host": "attacker.example",
"x-forwarded-proto": "https",
},
});
assert.equal(trustsForwardedHeaders(request), false);
assert.equal(validateBrowserMutationOrigin(request).ok, false);
});
it("fails closed for unknown proxy trust mode values", () => {
process.env.OMNIROUTE_TRUST_PROXY = "flase";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions", {
headers: {
...stampedPeer("127.0.0.1"),
origin: "https://gateway.example.test",
"x-forwarded-host": "gateway.example.test",
"x-forwarded-proto": "https",
},
});
assert.equal(trustsForwardedHeaders(request), false);
assert.equal(validateBrowserMutationOrigin(request).ok, false);
});
it("can trust forwarded headers from a token-stamped loopback proxy when explicitly enabled", () => {
process.env.OMNIROUTE_TRUST_PROXY = "true";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions", {
headers: {
...stampedPeer("127.0.0.1"),
origin: "https://gateway.example.test",
"x-forwarded-host": "gateway.example.test",
"x-forwarded-proto": "https",
},
});
assert.equal(trustsForwardedHeaders(request), true);
assert.equal(validateBrowserMutationOrigin(request).ok, true);
});
it("does not allow trusted forwarded headers to widen a configured public origin", () => {
process.env.NEXT_PUBLIC_BASE_URL = "https://gateway.example.test";
process.env.OMNIROUTE_TRUST_PROXY = "true";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions", {
headers: {
...stampedPeer("127.0.0.1"),
origin: "https://evil.example.test",
"x-forwarded-host": "evil.example.test",
"x-forwarded-proto": "https",
},
});
assert.equal(
getPublicOriginCandidates(request).some(
(candidate) => candidate.origin === "https://evil.example.test"
),
false
);
assert.equal(validateBrowserMutationOrigin(request).ok, false);
});
it("does not derive trusted forwarded origin from the raw host header", () => {
process.env.OMNIROUTE_TRUST_PROXY = "true";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions", {
headers: {
...stampedPeer("127.0.0.1"),
host: "gateway.example.test",
origin: "https://gateway.example.test",
"x-forwarded-proto": "https",
},
});
assert.equal(
getPublicOriginCandidates(request).some(
(candidate) => candidate.source === "trusted-forwarded"
),
false
);
assert.equal(validateBrowserMutationOrigin(request).ok, false);
});
it("rejects malformed forwarded origins even when proxy trust is enabled", () => {
process.env.OMNIROUTE_TRUST_PROXY = "true";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions", {
headers: {
...stampedPeer("127.0.0.1"),
origin: "https://gateway.example.test",
"x-forwarded-host": "gateway.example.test/evil",
"x-forwarded-proto": "https",
},
});
assert.equal(validateBrowserMutationOrigin(request).ok, false);
});
it("rejects cross-site fetch metadata before origin candidate matching", () => {
process.env.NEXT_PUBLIC_BASE_URL = "https://gateway.example.test";
const request = new Request("http://omniroute:20128/api/providers/health-autopilot/actions", {
headers: {
origin: "https://gateway.example.test",
"sec-fetch-site": "cross-site",
},
});
assert.deepEqual(validateBrowserMutationOrigin(request), {
ok: false,
reason: "cross-site-fetch-metadata",
});
});
});