Harden request origin checks and configure allowed hosts
This commit is contained in:
180
tests/request-origin.test.ts
Normal file
180
tests/request-origin.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { requestOrigin, verifyOrigin } from "../lib/request-origin";
|
||||
const internalUrl = "http://localhost:3000/api/v1/tokens";
|
||||
const makeRequest = (
|
||||
host: string,
|
||||
origin?: string,
|
||||
extra: Record<string, string> = {},
|
||||
) =>
|
||||
new Request(internalUrl, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
host,
|
||||
"content-type": "application/json",
|
||||
...(origin === undefined ? {} : { origin }),
|
||||
...extra,
|
||||
},
|
||||
});
|
||||
|
||||
test("the testing switch accepts any origin only when explicitly enabled", () => {
|
||||
const request = makeRequest("localhost:3000", "https://test-domain.example");
|
||||
assert.doesNotThrow(() => verifyOrigin(request, { allowAnyOrigin: true }));
|
||||
for (const configuration of [{}, { allowAnyOrigin: false }]) {
|
||||
assert.throws(
|
||||
() => verifyOrigin(request, configuration),
|
||||
/Invalid request origin/,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("the environment switch requires the exact value true", () => {
|
||||
const previous = process.env.MINEGEO_ALLOW_ANY_ORIGIN;
|
||||
const request = makeRequest("localhost:3000", "https://test-domain.example");
|
||||
try {
|
||||
for (const value of [undefined, "false", "0", "TRUE", "true"]) {
|
||||
if (value === undefined) delete process.env.MINEGEO_ALLOW_ANY_ORIGIN;
|
||||
else process.env.MINEGEO_ALLOW_ANY_ORIGIN = value;
|
||||
if (value === "true") assert.doesNotThrow(() => verifyOrigin(request));
|
||||
else assert.throws(() => verifyOrigin(request), /Invalid request origin/);
|
||||
}
|
||||
} finally {
|
||||
if (previous === undefined) delete process.env.MINEGEO_ALLOW_ANY_ORIGIN;
|
||||
else process.env.MINEGEO_ALLOW_ANY_ORIGIN = previous;
|
||||
}
|
||||
});
|
||||
|
||||
test("the testing switch retains JSON checks for requests without an origin", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyOrigin(
|
||||
new Request(internalUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
}),
|
||||
{ allowAnyOrigin: true },
|
||||
),
|
||||
/JSON request required/,
|
||||
);
|
||||
});
|
||||
|
||||
test("accepts a Mac LAN origin when Next uses localhost internally", () => {
|
||||
const request = makeRequest("192.168.1.50:3000", "http://192.168.1.50:3000");
|
||||
assert.equal(requestOrigin(request), "http://192.168.1.50:3000");
|
||||
assert.equal(
|
||||
requestOrigin(request, "http://localhost:3000/"),
|
||||
"http://192.168.1.50:3000",
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
verifyOrigin(request, { websiteUrl: "http://localhost:3000/" }),
|
||||
);
|
||||
});
|
||||
test("matches ports exactly and rejects cross-site and internal-address origins", () => {
|
||||
for (const origin of [
|
||||
"http://192.168.1.50:3001",
|
||||
"https://192.168.1.50:3000",
|
||||
"https://unrelated.example",
|
||||
"http://localhost:3000",
|
||||
]) {
|
||||
assert.throws(
|
||||
() => verifyOrigin(makeRequest("192.168.1.50:3000", origin), {}),
|
||||
/Invalid request origin/,
|
||||
);
|
||||
}
|
||||
});
|
||||
test("normalizes explicit public URLs and supports configured proxy origins", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
verifyOrigin(makeRequest("localhost:3000", "https://minegeo.example"), {
|
||||
websiteUrl: "https://minegeo.example/",
|
||||
}),
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
verifyOrigin(makeRequest("localhost:3000", "https://preview.example"), {
|
||||
additionalOrigins: " https://preview.example/ , https://other.example ",
|
||||
}),
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyOrigin(makeRequest("localhost:3000", "https://unrelated.example"), {
|
||||
websiteUrl: "https://minegeo.example/",
|
||||
}),
|
||||
/Invalid request origin/,
|
||||
);
|
||||
});
|
||||
test("forwarded hosts cannot authorize an unrelated origin", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyOrigin(
|
||||
makeRequest("192.168.1.50:3000", "https://unrelated.example", {
|
||||
"x-forwarded-host": "unrelated.example",
|
||||
"x-forwarded-proto": "https",
|
||||
}),
|
||||
{},
|
||||
),
|
||||
/Invalid request origin/,
|
||||
);
|
||||
});
|
||||
|
||||
test("login redirects preserve a configured public host's HTTPS scheme", () => {
|
||||
assert.equal(
|
||||
requestOrigin(makeRequest("minegeo.example"), "https://minegeo.example"),
|
||||
"https://minegeo.example",
|
||||
);
|
||||
});
|
||||
test("handles IPv6 and default ports without authorizing malformed authorities", () => {
|
||||
assert.doesNotThrow(() =>
|
||||
verifyOrigin(makeRequest("[::1]:3000", "http://[::1]:3000"), {}),
|
||||
);
|
||||
assert.doesNotThrow(() =>
|
||||
verifyOrigin(
|
||||
makeRequest("minegeo.example:80", "http://minegeo.example"),
|
||||
{},
|
||||
),
|
||||
);
|
||||
for (const host of [
|
||||
"trusted.example@unrelated.example",
|
||||
"trusted.example/path",
|
||||
"trusted.example,unrelated.example",
|
||||
"trusted.example:invalid-port",
|
||||
])
|
||||
assert.throws(
|
||||
() => requestOrigin(makeRequest(host)),
|
||||
/Invalid request origin/,
|
||||
);
|
||||
});
|
||||
test("keeps Java JSON and bearer requests usable without a browser Origin", () => {
|
||||
assert.doesNotThrow(() => verifyOrigin(makeRequest("localhost:3000"), {}));
|
||||
assert.doesNotThrow(() =>
|
||||
verifyOrigin(
|
||||
new Request(internalUrl, {
|
||||
method: "POST",
|
||||
headers: { authorization: "Bearer test" },
|
||||
}),
|
||||
{},
|
||||
),
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
verifyOrigin(
|
||||
new Request(internalUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
}),
|
||||
{},
|
||||
),
|
||||
/JSON request required/,
|
||||
);
|
||||
});
|
||||
test("opaque, credential-bearing, and path-bearing origins cannot bypass validation", () => {
|
||||
for (const origin of [
|
||||
"null",
|
||||
"http://name:password@localhost:3000",
|
||||
"http://localhost:3000/extra",
|
||||
"http://localhost:3000?extra=1",
|
||||
]) {
|
||||
assert.throws(
|
||||
() => verifyOrigin(makeRequest("localhost:3000", origin), {}),
|
||||
/Invalid request origin/,
|
||||
);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user