mirror of https://github.com/openclaw/openclaw.git
test: harden guarded fetch redirect coverage
This commit is contained in:
parent
f3d4bb4103
commit
5aa79f1ba4
|
|
@ -13,6 +13,34 @@ function okResponse(body = "ok"): Response {
|
||||||
return new Response(body, { status: 200 });
|
return new Response(body, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSecondRequestHeaders(fetchImpl: ReturnType<typeof vi.fn>): Headers {
|
||||||
|
const [, secondInit] = fetchImpl.mock.calls[1] as [string, RequestInit];
|
||||||
|
return new Headers(secondInit.headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectRedirectFailure(params: {
|
||||||
|
url: string;
|
||||||
|
responses: Response[];
|
||||||
|
expectedError: RegExp;
|
||||||
|
lookupFn?: NonNullable<Parameters<typeof fetchWithSsrFGuard>[0]["lookupFn"]>;
|
||||||
|
maxRedirects?: number;
|
||||||
|
}) {
|
||||||
|
const fetchImpl = vi.fn();
|
||||||
|
for (const response of params.responses) {
|
||||||
|
fetchImpl.mockResolvedValueOnce(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
fetchWithSsrFGuard({
|
||||||
|
url: params.url,
|
||||||
|
fetchImpl,
|
||||||
|
...(params.lookupFn ? { lookupFn: params.lookupFn } : {}),
|
||||||
|
...(params.maxRedirects === undefined ? {} : { maxRedirects: params.maxRedirects }),
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(params.expectedError);
|
||||||
|
return fetchImpl;
|
||||||
|
}
|
||||||
|
|
||||||
describe("fetchWithSsrFGuard hardening", () => {
|
describe("fetchWithSsrFGuard hardening", () => {
|
||||||
type LookupFn = NonNullable<Parameters<typeof fetchWithSsrFGuard>[0]["lookupFn"]>;
|
type LookupFn = NonNullable<Parameters<typeof fetchWithSsrFGuard>[0]["lookupFn"]>;
|
||||||
const CROSS_ORIGIN_REDIRECT_STRIPPED_HEADERS = [
|
const CROSS_ORIGIN_REDIRECT_STRIPPED_HEADERS = [
|
||||||
|
|
@ -33,11 +61,6 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||||
const createPublicLookup = (): LookupFn =>
|
const createPublicLookup = (): LookupFn =>
|
||||||
vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]) as unknown as LookupFn;
|
vi.fn(async () => [{ address: "93.184.216.34", family: 4 }]) as unknown as LookupFn;
|
||||||
|
|
||||||
const getSecondRequestHeaders = (fetchImpl: ReturnType<typeof vi.fn>): Headers => {
|
|
||||||
const [, secondInit] = fetchImpl.mock.calls[1] as [string, RequestInit];
|
|
||||||
return new Headers(secondInit.headers);
|
|
||||||
};
|
|
||||||
|
|
||||||
async function runProxyModeDispatcherTest(params: {
|
async function runProxyModeDispatcherTest(params: {
|
||||||
mode: (typeof GUARDED_FETCH_MODE)[keyof typeof GUARDED_FETCH_MODE];
|
mode: (typeof GUARDED_FETCH_MODE)[keyof typeof GUARDED_FETCH_MODE];
|
||||||
expectEnvProxy: boolean;
|
expectEnvProxy: boolean;
|
||||||
|
|
@ -112,15 +135,12 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||||
|
|
||||||
it("blocks redirect chains that hop to private hosts", async () => {
|
it("blocks redirect chains that hop to private hosts", async () => {
|
||||||
const lookupFn = createPublicLookup();
|
const lookupFn = createPublicLookup();
|
||||||
const fetchImpl = vi.fn().mockResolvedValueOnce(redirectResponse("http://127.0.0.1:6379/"));
|
const fetchImpl = await expectRedirectFailure({
|
||||||
|
url: "https://public.example/start",
|
||||||
await expect(
|
responses: [redirectResponse("http://127.0.0.1:6379/")],
|
||||||
fetchWithSsrFGuard({
|
expectedError: /private|internal|blocked/i,
|
||||||
url: "https://public.example/start",
|
lookupFn,
|
||||||
fetchImpl,
|
});
|
||||||
lookupFn,
|
|
||||||
}),
|
|
||||||
).rejects.toThrow(/private|internal|blocked/i);
|
|
||||||
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -136,6 +156,18 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||||
expect(fetchImpl).not.toHaveBeenCalled();
|
expect(fetchImpl).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not let wildcard allowlists match the apex host", async () => {
|
||||||
|
const fetchImpl = vi.fn();
|
||||||
|
await expect(
|
||||||
|
fetchWithSsrFGuard({
|
||||||
|
url: "https://assets.example.com/pic.png",
|
||||||
|
fetchImpl,
|
||||||
|
policy: { hostnameAllowlist: ["*.assets.example.com"] },
|
||||||
|
}),
|
||||||
|
).rejects.toThrow(/allowlist/i);
|
||||||
|
expect(fetchImpl).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("allows wildcard allowlisted hosts", async () => {
|
it("allows wildcard allowlisted hosts", async () => {
|
||||||
const lookupFn = createPublicLookup();
|
const lookupFn = createPublicLookup();
|
||||||
const fetchImpl = vi.fn(async () => new Response("ok", { status: 200 }));
|
const fetchImpl = vi.fn(async () => new Response("ok", { status: 200 }));
|
||||||
|
|
@ -211,6 +243,41 @@ describe("fetchWithSsrFGuard hardening", () => {
|
||||||
await result.release();
|
await result.release();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{
|
||||||
|
name: "rejects redirects without a location header",
|
||||||
|
responses: [new Response(null, { status: 302 })],
|
||||||
|
expectedError: /missing location header/i,
|
||||||
|
maxRedirects: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rejects redirect loops",
|
||||||
|
responses: [
|
||||||
|
redirectResponse("https://public.example/next"),
|
||||||
|
redirectResponse("https://public.example/next"),
|
||||||
|
],
|
||||||
|
expectedError: /redirect loop/i,
|
||||||
|
maxRedirects: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "rejects too many redirects",
|
||||||
|
responses: [
|
||||||
|
redirectResponse("https://public.example/one"),
|
||||||
|
redirectResponse("https://public.example/two"),
|
||||||
|
],
|
||||||
|
expectedError: /too many redirects/i,
|
||||||
|
maxRedirects: 1,
|
||||||
|
},
|
||||||
|
])("$name", async ({ responses, expectedError, maxRedirects }) => {
|
||||||
|
await expectRedirectFailure({
|
||||||
|
url: "https://public.example/start",
|
||||||
|
responses,
|
||||||
|
expectedError,
|
||||||
|
lookupFn: createPublicLookup(),
|
||||||
|
maxRedirects,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("ignores env proxy by default to preserve DNS-pinned destination binding", async () => {
|
it("ignores env proxy by default to preserve DNS-pinned destination binding", async () => {
|
||||||
await runProxyModeDispatcherTest({
|
await runProxyModeDispatcherTest({
|
||||||
mode: GUARDED_FETCH_MODE.STRICT,
|
mode: GUARDED_FETCH_MODE.STRICT,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue