181 lines
5.5 KiB
TypeScript
181 lines
5.5 KiB
TypeScript
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/,
|
|
);
|
|
}
|
|
});
|