Files
SillyTavern/tests/private-request-filter.test.js
Cohee 338119ab77 Implement private IP range request host validator (#5497)
* feat: implement private IP range request host validator for server-side HTTP requests

* feat: add link-local address support

* fix: use correct config keys

* fix: if config missing use default loopback addresses

* fix: re-use resolved address for connection

* test: add unit coverage for private request filter and proxy interaction

Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/1813593e-2263-45e2-aa53-74d39515f1df

Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>

* test: remove request-proxy.test.js

* perf: cache resolved matches

* fix: remove unused import

* fix: use proper ipv4 loopback cidr

* fix: correct raiseError comment

* test: uses tls.connect for secure endpoints

* Implement private IP range request host validator

Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/e76ba122-136e-43ad-b4bc-ea48a01fcdda

Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>

* Revert "Implement private IP range request host validator"

This reverts commit 14e271470227b485b7d23caac31a237abf9f7835.

* fix: close request without sending status in CORS forwarding when headers were sent

* fix: not enabled -> disabled

* feat: add enableKeepAlive option to PrivateRequestAgent

Co-authored-by: Copilot <copilot@github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
2026-04-27 01:51:18 +03:00

131 lines
4.9 KiB
JavaScript

import { describe, test, expect, jest, beforeAll, beforeEach, afterAll } from '@jest/globals';
const mockNetConnect = jest.fn(() => ({ type: 'net-socket' }));
const mockTlsConnect = jest.fn(() => ({ type: 'tls-socket' }));
const mockLookup = jest.fn();
jest.unstable_mockModule('node:net', () => ({
default: { connect: mockNetConnect },
}));
jest.unstable_mockModule('node:tls', () => ({
default: { connect: mockTlsConnect },
}));
jest.unstable_mockModule('node:dns', () => ({
default: { promises: { lookup: mockLookup } },
}));
jest.unstable_mockModule('../src/util.js', () => ({
color: {
red: text => text,
green: text => text,
blue: text => text,
yellow: text => text,
},
}));
jest.unstable_mockModule('../src/express-common.js', () => ({
filterValidIpPatterns: patterns => patterns,
}));
/** @type {import('../src/private-request-filter.js').default} */
let initPrivateRequestFilter;
/** @type {import('node:http').default} */
let http;
/** @type {import('node:https').default} */
let https;
let originalHttpGlobalAgent;
let originalHttpsGlobalAgent;
beforeAll(async () => {
({ default: initPrivateRequestFilter } = await import('../src/private-request-filter.js'));
({ default: http } = await import('node:http'));
({ default: https } = await import('node:https'));
originalHttpGlobalAgent = http.globalAgent;
originalHttpsGlobalAgent = https.globalAgent;
});
beforeEach(() => {
mockNetConnect.mockClear();
mockTlsConnect.mockClear();
mockLookup.mockReset();
http.globalAgent = originalHttpGlobalAgent;
https.globalAgent = originalHttpsGlobalAgent;
});
afterAll(() => {
http.globalAgent = originalHttpGlobalAgent;
https.globalAgent = originalHttpsGlobalAgent;
});
function initAgent({ privateAddressWhitelist = [], allowUnresolvedHosts = false } = {}) {
initPrivateRequestFilter({
listen: false,
enabled: true,
privateAddressWhitelist,
logBlocked: false,
logAllowed: false,
allowUnresolvedHosts,
});
return http.globalAgent;
}
describe('private request filter', () => {
test('allows direct private IP requests only when whitelisted', async () => {
const agent = initAgent({ privateAddressWhitelist: ['127.0.0.0/8'] });
await agent.connect({}, { host: '127.0.0.1', secureEndpoint: false });
expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '127.0.0.1' }));
const blockedAgent = initAgent({ privateAddressWhitelist: [] });
await expect(blockedAgent.connect({}, { host: '127.0.0.1', secureEndpoint: false }))
.rejects
.toThrow('Blocked request to private IP address: 127.0.0.1');
});
test('resolves hostnames and blocks when DNS returns private IP', async () => {
mockLookup.mockResolvedValue({ address: '192.168.1.8' });
const agent = initAgent();
await expect(agent.connect({}, { host: 'example.com', secureEndpoint: false }))
.rejects
.toThrow('Blocked request to private IP address: 192.168.1.8');
expect(mockNetConnect).not.toHaveBeenCalled();
});
test('connects to resolved public IP to avoid hostname re-resolution', async () => {
mockLookup.mockResolvedValue({ address: '93.184.216.34' });
const agent = initAgent();
await agent.connect({}, { host: 'example.com', secureEndpoint: false });
expect(mockLookup).toHaveBeenCalledWith('example.com');
expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '93.184.216.34' }));
});
test('handles unresolved hosts according to allowUnresolvedHosts setting', async () => {
mockLookup.mockRejectedValue(new Error('lookup failed'));
const blockedAgent = initAgent({ allowUnresolvedHosts: false });
await expect(blockedAgent.connect({}, { host: 'missing-host.local', secureEndpoint: false }))
.rejects
.toThrow('Unable to resolve host: missing-host.local. Set privateAddressWhitelist.allowUnresolvedHosts to true to bypass this check.');
expect(mockNetConnect).not.toHaveBeenCalled();
const allowedAgent = initAgent({ allowUnresolvedHosts: true });
await allowedAgent.connect({}, { host: 'missing-host.local', secureEndpoint: false });
expect(mockNetConnect).toHaveBeenCalledWith(expect.objectContaining({ host: 'missing-host.local' }));
});
test('uses tls.connect for secure endpoints', async () => {
mockLookup.mockResolvedValue({ address: '93.184.216.34' });
const agent = initAgent();
await agent.connect({}, { host: 'example.com', secureEndpoint: true });
expect(mockLookup).toHaveBeenCalledWith('example.com');
expect(mockTlsConnect).toHaveBeenCalledWith(expect.objectContaining({ host: '93.184.216.34' }));
expect(mockNetConnect).not.toHaveBeenCalled();
});
});