Files
Void-Homelab/tests/ingest/safe_fetch.test.js
root afc20712cb feat(api): capture POST + upload + SSRF-safe URL fetch
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>
2026-06-01 03:42:54 +10:00

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);
});
});