safe_fetch.js validates URLs before fetch: rejects non-http(s), literal or DNS-resolved loopback / RFC1918 / link-local / CGNAT / metadata addresses; follows redirects manually with the same checks on each hop. Test fixtures gate the check with VOID_INGEST_ALLOW_PRIVATE for offline fixtures that hit 127.0.0.1. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
35 lines
1.3 KiB
JavaScript
35 lines
1.3 KiB
JavaScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { safeFetch, SafeFetchError } from '../../lib/ingest/safe_fetch.js';
|
|
|
|
beforeEach(() => { delete process.env.VOID_INGEST_ALLOW_PRIVATE; });
|
|
afterEach(() => { vi.restoreAllMocks(); });
|
|
|
|
describe('safeFetch', () => {
|
|
it('rejects file:// scheme', async () => {
|
|
await expect(safeFetch('file:///etc/passwd')).rejects.toThrow(SafeFetchError);
|
|
});
|
|
|
|
it('rejects literal loopback IP', async () => {
|
|
await expect(safeFetch('http://127.0.0.1/x')).rejects.toThrow(/blocked/);
|
|
});
|
|
|
|
it('rejects literal RFC1918 IP', async () => {
|
|
await expect(safeFetch('http://192.168.1.1/x')).rejects.toThrow(/blocked/);
|
|
});
|
|
|
|
it('rejects literal CGNAT IP', async () => {
|
|
await expect(safeFetch('http://100.64.0.1/x')).rejects.toThrow(/blocked/);
|
|
});
|
|
|
|
it('rejects AWS metadata literal IP', async () => {
|
|
await expect(safeFetch('http://169.254.169.254/latest/meta-data/')).rejects.toThrow(/blocked/);
|
|
});
|
|
|
|
it('allows literal IPs when VOID_INGEST_ALLOW_PRIVATE=true (test fixtures)', async () => {
|
|
process.env.VOID_INGEST_ALLOW_PRIVATE = 'true';
|
|
global.fetch = vi.fn(async () => new Response('ok', { status: 200 }));
|
|
const res = await safeFetch('http://127.0.0.1:65535/');
|
|
expect(res.status).toBe(200);
|
|
});
|
|
});
|