Re-check SSRF on every redirect hop when resolving short links

Replace the one-shot checkSsrf + fetch(redirect:'follow') in the maps and place short-link resolvers with safeFetchFollow, which follows redirects manually and re-runs checkSsrf against the DNS-pinned IP of each hop (max 5). A redirect to an internal/loopback address is now blocked even when the initial URL is public, while legitimate cross-host redirects (goo.gl -> maps.google.com) still resolve.
This commit is contained in:
Maurice
2026-05-31 15:43:59 +02:00
parent 5a0124e7cb
commit 460694e335
5 changed files with 254 additions and 20 deletions
+20 -3
View File
@@ -29,9 +29,26 @@ vi.mock('../../../src/db/database', () => ({
},
}));
vi.mock('../../../src/utils/ssrfGuard', () => ({
checkSsrf: mockCheckSsrf,
}));
vi.mock('../../../src/utils/ssrfGuard', () => {
class SsrfBlockedError extends Error {
constructor(message: string) {
super(message);
this.name = 'SsrfBlockedError';
}
}
return {
checkSsrf: mockCheckSsrf,
SsrfBlockedError,
// Mirror the real per-hop helper closely enough for unit tests: run the
// (mocked) SSRF check, then fetch through the (stubbed) global fetch. The
// fetch stubs in these tests already return the final resolved response.
safeFetchFollow: vi.fn(async (url: string, init?: any) => {
const ssrf = await mockCheckSsrf(url);
if (!ssrf.allowed) throw new SsrfBlockedError(ssrf.error ?? 'Request blocked by SSRF guard');
return (globalThis.fetch as any)(url, init);
}),
};
});
vi.mock('../../../src/services/apiKeyCrypto', () => ({
decrypt_api_key: (v: string | null) => v,