mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
5138358d7b
Place photos, link previews, webhooks and the integrations fetch through the guard's own dispatcher and so ignored the outbound proxy, which left a server that can only reach the internet through one without them. They now read HTTP_PROXY, HTTPS_PROXY and NO_PROXY themselves and connect through the proxy, after the guard has vetted the target. The Helm chart passes the three variables through. (#1754)
82 lines
3.6 KiB
TypeScript
82 lines
3.6 KiB
TypeScript
/**
|
|
* Guarded requests and the outbound proxy (#1754). The SSRF guard fetches through
|
|
* its own dispatcher, so it used to skip HTTP_PROXY entirely; these pin when it
|
|
* goes through the proxy and when NO_PROXY keeps it direct.
|
|
*/
|
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
const built = vi.hoisted(() => ({ agents: [] as unknown[], proxies: [] as Array<Record<string, unknown>> }));
|
|
|
|
vi.mock('undici', () => ({
|
|
Agent: class { constructor(opts: unknown) { built.agents.push(opts); } },
|
|
ProxyAgent: class { constructor(opts: Record<string, unknown>) { built.proxies.push(opts); } },
|
|
}));
|
|
|
|
const PROXY_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'NO_PROXY', 'http_proxy', 'https_proxy', 'no_proxy'] as const;
|
|
|
|
async function loadGuard(env: Partial<Record<(typeof PROXY_KEYS)[number], string>>) {
|
|
for (const key of PROXY_KEYS) delete process.env[key];
|
|
Object.assign(process.env, env);
|
|
vi.resetModules();
|
|
return import('../../../src/utils/ssrfGuard');
|
|
}
|
|
|
|
afterEach(() => {
|
|
for (const key of PROXY_KEYS) delete process.env[key];
|
|
built.agents.length = 0;
|
|
built.proxies.length = 0;
|
|
});
|
|
|
|
describe('proxyFor', () => {
|
|
it('SSRF-PROXY-001: no proxy configured means a direct connection', async () => {
|
|
const { proxyFor } = await loadGuard({});
|
|
expect(proxyFor('https://example.com/a')).toBeNull();
|
|
});
|
|
|
|
it('SSRF-PROXY-002: picks the proxy by scheme, HTTPS falling back to HTTP_PROXY', async () => {
|
|
const { proxyFor } = await loadGuard({ HTTP_PROXY: 'http://plain:3128' });
|
|
expect(proxyFor('http://example.com/')).toBe('http://plain:3128');
|
|
expect(proxyFor('https://example.com/')).toBe('http://plain:3128');
|
|
const both = await loadGuard({ HTTP_PROXY: 'http://plain:3128', https_proxy: 'http://secure:3129' });
|
|
expect(both.proxyFor('https://example.com/')).toBe('http://secure:3129');
|
|
});
|
|
|
|
it('SSRF-PROXY-003: NO_PROXY matches the host, its subdomains, a port and the wildcard', async () => {
|
|
const { proxyFor } = await loadGuard({
|
|
HTTPS_PROXY: 'http://proxy:3128',
|
|
NO_PROXY: 'localhost, .internal.lan, photos.example.com:8443',
|
|
});
|
|
expect(proxyFor('https://localhost/health')).toBeNull();
|
|
expect(proxyFor('https://immich.internal.lan/api')).toBeNull();
|
|
expect(proxyFor('https://internal.lan/')).toBeNull();
|
|
expect(proxyFor('https://photos.example.com:8443/x')).toBeNull();
|
|
// Same host on another port, and a lookalike domain, still go through the proxy.
|
|
expect(proxyFor('https://photos.example.com/x')).toBe('http://proxy:3128');
|
|
expect(proxyFor('https://notinternal.lan/')).toBe('http://proxy:3128');
|
|
|
|
const all = await loadGuard({ HTTPS_PROXY: 'http://proxy:3128', NO_PROXY: '*' });
|
|
expect(all.proxyFor('https://example.com/')).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('createOutboundDispatcher', () => {
|
|
it('SSRF-PROXY-004: without a proxy it is the pinned agent', async () => {
|
|
const { createOutboundDispatcher } = await loadGuard({});
|
|
createOutboundDispatcher('https://example.com/', ['93.184.216.34']);
|
|
expect(built.agents).toHaveLength(1);
|
|
expect(built.proxies).toHaveLength(0);
|
|
});
|
|
|
|
it('SSRF-PROXY-005: with a proxy it goes through it, keeping TLS checks and the timeout', async () => {
|
|
const { createOutboundDispatcher } = await loadGuard({ HTTPS_PROXY: 'http://proxy:3128' });
|
|
createOutboundDispatcher('https://example.com/', ['93.184.216.34'], false, 30_000);
|
|
expect(built.agents).toHaveLength(0);
|
|
expect(built.proxies[0]).toEqual({
|
|
uri: 'http://proxy:3128',
|
|
requestTls: { rejectUnauthorized: false },
|
|
headersTimeout: 30_000,
|
|
bodyTimeout: 30_000,
|
|
});
|
|
});
|
|
});
|