OpenMAIC-React / tests /server /ssrf-guard.test.ts
muthuk1's picture
Add missing files: LICENSE, Dockerfile, .github, tests, e2e, eval, scripts, configs
a0ebf39 verified
import { beforeEach, describe, expect, it, vi } from 'vitest';
const { lookupMock } = vi.hoisted(() => ({
lookupMock: vi.fn(),
}));
vi.mock('node:dns', () => ({
promises: {
lookup: lookupMock,
},
}));
describe('validateUrlForSSRF', () => {
beforeEach(() => {
vi.resetModules();
lookupMock.mockReset();
});
it('allows a public hostname when DNS resolves to a public IP', async () => {
lookupMock.mockResolvedValue([{ address: '93.184.216.34', family: 4 }]);
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
await expect(validateUrlForSSRF('https://api.openai.com')).resolves.toBeNull();
expect(lookupMock).toHaveBeenCalledWith('api.openai.com', { all: true, verbatim: true });
});
it('allows a public IP literal without DNS lookup', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
await expect(validateUrlForSSRF('https://8.8.8.8')).resolves.toBeNull();
expect(lookupMock).not.toHaveBeenCalled();
});
it('allows a public IPv6 literal without DNS lookup', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
await expect(validateUrlForSSRF('https://[2606:4700:4700::1111]')).resolves.toBeNull();
expect(lookupMock).not.toHaveBeenCalled();
});
it('rejects invalid URLs and non-http protocols', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
await expect(validateUrlForSSRF('not-a-url')).resolves.toBe('Invalid URL');
await expect(validateUrlForSSRF('ftp://example.com')).resolves.toBe(
'Only HTTP(S) URLs are allowed',
);
await expect(validateUrlForSSRF('file:///etc/passwd')).resolves.toBe(
'Only HTTP(S) URLs are allowed',
);
await expect(validateUrlForSSRF('javascript:alert(1)')).resolves.toBe(
'Only HTTP(S) URLs are allowed',
);
});
it('rejects blocked hostnames immediately', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
await expect(validateUrlForSSRF('http://localhost')).resolves.toBe(
'Local/private network URLs are not allowed',
);
await expect(validateUrlForSSRF('http://printer.local')).resolves.toBe(
'Local/private network URLs are not allowed',
);
expect(lookupMock).not.toHaveBeenCalled();
});
it('rejects private IPv4 literals', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
const urls = [
'http://127.0.0.1',
'http://10.0.0.42',
'http://172.16.5.4',
'http://172.31.255.255',
'http://192.168.1.10',
'http://169.254.169.254',
'http://0.0.0.0',
];
for (const url of urls) {
await expect(validateUrlForSSRF(url)).resolves.toBe(
'Local/private network URLs are not allowed',
);
}
expect(lookupMock).not.toHaveBeenCalled();
});
it('rejects private IPv6 literals and mapped loopback addresses', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
const urls = [
'http://[::1]',
'http://[fd00::1234]',
'http://[fe80::1]',
'http://[fec0::1]',
'http://[::ffff:127.0.0.1]',
];
for (const url of urls) {
await expect(validateUrlForSSRF(url)).resolves.toBe(
'Local/private network URLs are not allowed',
);
}
expect(lookupMock).not.toHaveBeenCalled();
});
it('rejects 6to4 tunnel addresses embedding private IPv4', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
// 2002:7f00:0001:: embeds 127.0.0.1
await expect(validateUrlForSSRF('http://[2002:7f00:0001::]')).resolves.toBe(
'Local/private network URLs are not allowed',
);
// 2002:0a00:0001:: embeds 10.0.0.1
await expect(validateUrlForSSRF('http://[2002:0a00:0001::]')).resolves.toBe(
'Local/private network URLs are not allowed',
);
expect(lookupMock).not.toHaveBeenCalled();
});
it('allows 6to4 tunnel addresses embedding public IPv4', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
// 2002:0808:0808:: embeds 8.8.8.8
await expect(validateUrlForSSRF('http://[2002:0808:0808::]')).resolves.toBeNull();
expect(lookupMock).not.toHaveBeenCalled();
});
it('rejects Teredo tunnel addresses embedding private IPv4', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
// Client IPv4 127.0.0.1 XOR 0xFFFFFFFF = 0x80FFFFFE → hextets 80ff:fffe
await expect(
validateUrlForSSRF('http://[2001:0000:4136:e378:8000:63bf:80ff:fffe]'),
).resolves.toBe('Local/private network URLs are not allowed');
expect(lookupMock).not.toHaveBeenCalled();
});
it('allows Teredo tunnel addresses embedding public IPv4', async () => {
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
// Client IPv4 8.8.8.8 XOR 0xFFFFFFFF = 0xF7F7F7F7 → hextets f7f7:f7f7
await expect(
validateUrlForSSRF('http://[2001:0000:4136:e378:8000:63bf:f7f7:f7f7]'),
).resolves.toBeNull();
expect(lookupMock).not.toHaveBeenCalled();
});
it('rejects hostnames that resolve to a private IP', async () => {
lookupMock.mockResolvedValue([{ address: '127.0.0.1', family: 4 }]);
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
await expect(validateUrlForSSRF('https://attacker.com')).resolves.toBe(
'Local/private network URLs are not allowed',
);
});
it('rejects hostnames when any DNS answer is private', async () => {
lookupMock.mockResolvedValue([
{ address: '93.184.216.34', family: 4 },
{ address: '192.168.1.10', family: 4 },
]);
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
await expect(validateUrlForSSRF('https://mixed.example')).resolves.toBe(
'Local/private network URLs are not allowed',
);
});
it('fails closed when DNS lookup errors', async () => {
lookupMock.mockRejectedValue(new Error('ENOTFOUND'));
const { validateUrlForSSRF } = await import('@/lib/server/ssrf-guard');
await expect(validateUrlForSSRF('https://missing.example')).resolves.toBe(
'Unable to verify hostname safety',
);
});
});