Blame Raw
Cohee · 51ad27fb · · 130 lines (4.9 KB)
1 contributor
1import { describe, test, expect, jest, beforeAll, beforeEach, afterAll } from '@jest/globals';
2
3const mockNetConnect = jest.fn(() => ({ type: 'net-socket' }));
4const mockTlsConnect = jest.fn(() => ({ type: 'tls-socket' }));
5const mockLookup = jest.fn();
6
7jest.unstable_mockModule('node:net', () => ({
8 default: { connect: mockNetConnect },
9}));
10
11jest.unstable_mockModule('node:tls', () => ({
12 default: { connect: mockTlsConnect },
13}));
14
15jest.unstable_mockModule('node:dns', () => ({
16 default: { promises: { lookup: mockLookup } },
17}));
18
19jest.unstable_mockModule('../src/util.js', () => ({
20 color: {
21 red: text => text,
22 green: text => text,
23 blue: text => text,
24 yellow: text => text,
25 },
26}));
27
28jest.unstable_mockModule('../src/express-common.js', () => ({
29 filterValidIpPatterns: patterns => patterns,
30}));
31
32/** @type {import('../src/private-request-filter.js').default} */
33let initPrivateRequestFilter;
34/** @type {import('node:http').default} */
35let http;
36/** @type {import('node:https').default} */
37let https;
38let originalHttpGlobalAgent;
39let originalHttpsGlobalAgent;
40
41beforeAll(async () => {
42 ({ default: initPrivateRequestFilter } = await import('../src/private-request-filter.js'));
43 ({ default: http } = await import('node:http'));
44 ({ default: https } = await import('node:https'));
45 originalHttpGlobalAgent = http.globalAgent;
46 originalHttpsGlobalAgent = https.globalAgent;
47});
48
49beforeEach(() => {
50 mockNetConnect.mockClear();
51 mockTlsConnect.mockClear();
52 mockLookup.mockReset();
53 http.globalAgent = originalHttpGlobalAgent;
54 https.globalAgent = originalHttpsGlobalAgent;
55});
56
57afterAll(() => {
58 http.globalAgent = originalHttpGlobalAgent;
59 https.globalAgent = originalHttpsGlobalAgent;
60});
61
62function initAgent({ privateAddressWhitelist = [], allowUnresolvedHosts = false } = {}) {
63 initPrivateRequestFilter({
64 listen: false,
65 enabled: true,
66 privateAddressWhitelist,
67 logBlocked: false,
68 logAllowed: false,
69 allowUnresolvedHosts,
70 });
71
72 return http.globalAgent;
73}
74
75describe('private request filter', () => {
76 test('allows direct private IP requests only when whitelisted', async () => {
77 const agent = initAgent({ privateAddressWhitelist: ['127.0.0.0/8'] });
78 await agent.connect({}, { host: '127.0.0.1', secureEndpoint: false });
79
80 expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '127.0.0.1' }));
81
82 const blockedAgent = initAgent({ privateAddressWhitelist: [] });
83 await expect(blockedAgent.connect({}, { host: '127.0.0.1', secureEndpoint: false }))
84 .rejects
85 .toThrow('Blocked request to private IP address: 127.0.0.1');
86 });
87
88 test('resolves hostnames and blocks when DNS returns private IP', async () => {
89 mockLookup.mockResolvedValue({ address: '192.168.1.8' });
90 const agent = initAgent();
91
92 await expect(agent.connect({}, { host: 'example.com', secureEndpoint: false }))
93 .rejects
94 .toThrow('Blocked request to private IP address: 192.168.1.8');
95 expect(mockNetConnect).not.toHaveBeenCalled();
96 });
97
98 test('connects to resolved public IP to avoid hostname re-resolution', async () => {
99 mockLookup.mockResolvedValue({ address: '93.184.216.34' });
100 const agent = initAgent();
101
102 await agent.connect({}, { host: 'example.com', secureEndpoint: false });
103
104 expect(mockLookup).toHaveBeenCalledWith('example.com');
105 expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '93.184.216.34' }));
106 });
107
108 test('handles unresolved hosts according to allowUnresolvedHosts setting', async () => {
109 mockLookup.mockRejectedValue(new Error('lookup failed'));
110 const blockedAgent = initAgent({ allowUnresolvedHosts: false });
111
112 await expect(blockedAgent.connect({}, { host: 'missing-host.local', secureEndpoint: false }))
113 .rejects
114 .toThrow('Unable to resolve host: missing-host.local. Set privateAddressWhitelist.allowUnresolvedHosts to true to bypass this check.');
115 expect(mockNetConnect).not.toHaveBeenCalled();
116
117 const allowedAgent = initAgent({ allowUnresolvedHosts: true });
118 await allowedAgent.connect({}, { host: 'missing-host.local', secureEndpoint: false });
119 expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: 'missing-host.local' }));
120 });
121
122 test('uses tls.connect for secure endpoints', async () => {
123 mockLookup.mockResolvedValue({ address: '93.184.216.34' });
124 const agent = initAgent();
125 await agent.connect({}, { host: 'example.com', secureEndpoint: true });
126 expect(mockLookup).toHaveBeenCalledWith('example.com');
127 expect(mockTlsConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '93.184.216.34' }));
128 expect(mockNetConnect).not.toHaveBeenCalled();
129 });
130});