@lobehub/chat
Version:
Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.
344 lines (278 loc) • 11.1 kB
text/typescript
// @ts-ignore
import fetch from 'node-fetch';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { ssrfSafeFetch } from './index';
// Mock node-fetch to avoid actual network requests
vi.mock('node-fetch', () => ({
default: vi.fn(),
}));
const mockFetch = fetch as any;
// Mock console.error to avoid noise in test output
vi.spyOn(console, 'error').mockImplementation(() => {});
describe('ssrfSafeFetch', () => {
const createMockResponse = (
options: {
arrayBuffer?: ArrayBuffer;
headers?: Map<string, string>;
status?: number;
statusText?: string;
} = {},
) => ({
arrayBuffer: vi.fn().mockResolvedValue(options.arrayBuffer || new ArrayBuffer(10)),
headers: options.headers || new Map(),
status: options.status || 200,
statusText: options.statusText || 'OK',
});
beforeEach(() => {
vi.clearAllMocks();
// Reset environment variables
delete process.env.SSRF_ALLOW_IP_ADDRESS_LIST;
delete process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS;
});
describe('successful requests to allowed URLs', () => {
it('should make a successful fetch request to external URL', async () => {
const mockResponse = createMockResponse({
headers: new Map([['content-type', 'application/json']]),
});
mockFetch.mockResolvedValue(mockResponse);
const response = await ssrfSafeFetch('https://httpbin.org/get');
expect(mockFetch).toHaveBeenCalledWith(
'https://httpbin.org/get',
expect.objectContaining({
agent: expect.objectContaining({
requestFilterOptions: expect.objectContaining({
allowIPAddressList: [],
allowMetaIPAddress: false,
allowPrivateIPAddress: false,
denyIPAddressList: [],
}),
}),
}),
);
expect(response).toBeInstanceOf(Response);
expect(response.status).toBe(200);
expect(response.statusText).toBe('OK');
});
it('should pass through request options', async () => {
const mockResponse = createMockResponse();
mockFetch.mockResolvedValue(mockResponse);
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ test: 'data' }),
};
await ssrfSafeFetch('https://httpbin.org/post', requestOptions);
expect(mockFetch).toHaveBeenCalledWith(
'https://httpbin.org/post',
expect.objectContaining({
...requestOptions,
agent: expect.objectContaining({
requestFilterOptions: expect.objectContaining({
allowIPAddressList: [],
allowMetaIPAddress: false,
allowPrivateIPAddress: false,
denyIPAddressList: [],
}),
}),
}),
);
});
});
describe('SSRF protection for private IP addresses', () => {
const privateIPs = [
'http://127.0.0.1:8080',
'http://localhost:3000',
'http://192.168.1.1/api',
'http://10.0.0.1/internal',
'http://172.16.0.1/admin',
'http://0.0.0.0:80',
];
privateIPs.forEach((url) => {
it(`should block requests to private IP: ${url}`, async () => {
// The request-filtering-agent should throw an error for private IPs
mockFetch.mockImplementation(() => {
throw new Error('getaddrinfo ENOTFOUND');
});
await expect(ssrfSafeFetch(url)).rejects.toThrow(/SSRF-safe fetch failed/);
expect(console.error).toHaveBeenCalledWith('SSRF-safe fetch error:', expect.any(Error));
});
});
it('should allow private IPs when SSRF_ALLOW_PRIVATE_IP_ADDRESS is true', async () => {
process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS = 'true';
const mockResponse = createMockResponse();
mockFetch.mockResolvedValue(mockResponse);
const response = await ssrfSafeFetch('http://127.0.0.1:8080/api');
expect(response).toBeInstanceOf(Response);
expect(response.status).toBe(200);
});
it('should allow specific IPs in SSRF_ALLOW_IP_ADDRESS_LIST', async () => {
process.env.SSRF_ALLOW_IP_ADDRESS_LIST = '127.0.0.1,192.168.1.100';
const mockResponse = createMockResponse();
mockFetch.mockResolvedValue(mockResponse);
const response = await ssrfSafeFetch('http://127.0.0.1:8080/api');
expect(response).toBeInstanceOf(Response);
expect(response.status).toBe(200);
});
});
describe('SSRF protection for malicious URLs', () => {
const maliciousUrls = [
'http://169.254.169.254/latest/meta-data/', // AWS metadata service
'http://169.254.169.254:80/computeMetadata/v1/', // GCP metadata
'http://metadata.google.internal/computeMetadata/v1/',
'file:///etc/passwd', // File protocol
'ftp://internal.company.com/secrets', // FTP protocol
];
maliciousUrls.forEach((url) => {
it(`should block malicious URL: ${url}`, async () => {
mockFetch.mockImplementation(() => {
throw new Error('Request blocked by SSRF protection');
});
await expect(ssrfSafeFetch(url)).rejects.toThrow(/SSRF-safe fetch failed/);
});
});
});
describe('environment variable configuration', () => {
it('should respect empty SSRF_ALLOW_IP_ADDRESS_LIST', async () => {
process.env.SSRF_ALLOW_IP_ADDRESS_LIST = '';
mockFetch.mockImplementation(() => {
throw new Error('getaddrinfo ENOTFOUND');
});
await expect(ssrfSafeFetch('http://127.0.0.1:8080')).rejects.toThrow(
/SSRF-safe fetch failed/,
);
});
it('should handle invalid environment variable values gracefully', async () => {
process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS = 'invalid-value';
mockFetch.mockImplementation(() => {
throw new Error('getaddrinfo ENOTFOUND');
});
// Should default to false when env var is not 'true'
await expect(ssrfSafeFetch('http://127.0.0.1:8080')).rejects.toThrow(
/SSRF-safe fetch failed/,
);
});
});
describe('error handling', () => {
it('should throw error with descriptive message when fetch fails', async () => {
const originalError = new Error('Network error');
mockFetch.mockRejectedValue(originalError);
await expect(ssrfSafeFetch('https://example.com')).rejects.toThrow(
'SSRF-safe fetch failed: Network error',
);
expect(console.error).toHaveBeenCalledWith('SSRF-safe fetch error:', originalError);
});
it('should handle non-Error thrown values', async () => {
const nonErrorValue = 'String error';
mockFetch.mockRejectedValue(nonErrorValue);
await expect(ssrfSafeFetch('https://example.com')).rejects.toThrow(
'SSRF-safe fetch failed: String error',
);
});
it('should handle null/undefined error values', async () => {
mockFetch.mockRejectedValue(null);
await expect(ssrfSafeFetch('https://example.com')).rejects.toThrow(
'SSRF-safe fetch failed: null',
);
});
});
describe('response conversion', () => {
it('should convert node-fetch Response to standard Response', async () => {
const mockArrayBuffer = new ArrayBuffer(10);
const mockHeaders = new Map([
['content-type', 'application/json'],
['content-length', '10'],
]);
const mockResponse = createMockResponse({
arrayBuffer: mockArrayBuffer,
headers: mockHeaders,
status: 201,
statusText: 'Created',
});
mockFetch.mockResolvedValue(mockResponse);
const response = await ssrfSafeFetch('https://httpbin.org/status/201');
expect(response).toBeInstanceOf(Response);
expect(response.status).toBe(201);
expect(response.statusText).toBe('Created');
expect(response.headers.get('content-type')).toBe('application/json');
expect(response.headers.get('content-length')).toBe('10');
expect(mockResponse.arrayBuffer).toHaveBeenCalled();
});
it('should handle response with different status codes', async () => {
const mockResponse = createMockResponse({
arrayBuffer: new ArrayBuffer(0),
status: 404,
statusText: 'Not Found',
});
mockFetch.mockResolvedValue(mockResponse);
const response = await ssrfSafeFetch('https://httpbin.org/status/404');
expect(response.status).toBe(404);
expect(response.statusText).toBe('Not Found');
});
it('should handle response with empty headers', async () => {
const mockResponse = createMockResponse({
arrayBuffer: new ArrayBuffer(0),
});
mockFetch.mockResolvedValue(mockResponse);
const response = await ssrfSafeFetch('https://httpbin.org/get');
expect(response.headers).toBeDefined();
expect([...response.headers.entries()]).toEqual([]);
});
});
describe('integration scenarios', () => {
it('should work with complex request configurations', async () => {
process.env.SSRF_ALLOW_IP_ADDRESS_LIST = '127.0.0.1';
process.env.SSRF_ALLOW_PRIVATE_IP_ADDRESS = 'true';
const mockResponse = createMockResponse({
// @ts-ignore
arrayBuffer: new TextEncoder().encode('{"success": true}'),
headers: new Map([['content-type', 'application/json']]),
});
mockFetch.mockResolvedValue(mockResponse);
const requestOptions = {
method: 'PUT',
headers: {
'Authorization': 'Bearer token123',
'Content-Type': 'application/json',
},
body: JSON.stringify({ data: 'test' }),
};
const response = await ssrfSafeFetch('https://api.example.com/data', requestOptions);
expect(mockFetch).toHaveBeenCalledWith(
'https://api.example.com/data',
expect.objectContaining({
...requestOptions,
agent: expect.objectContaining({
requestFilterOptions: expect.objectContaining({
allowIPAddressList: ['127.0.0.1'],
allowMetaIPAddress: true,
allowPrivateIPAddress: true,
denyIPAddressList: [],
}),
}),
}),
);
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('application/json');
});
it('should properly handle agent function with HTTPS URLs', async () => {
const mockResponse = createMockResponse();
mockFetch.mockResolvedValue(mockResponse);
await ssrfSafeFetch('https://secure.example.com/api');
// Verify that the agent is properly configured for HTTPS
expect(mockFetch).toHaveBeenCalledWith(
'https://secure.example.com/api',
expect.objectContaining({
agent: expect.objectContaining({
protocol: 'https:',
requestFilterOptions: expect.objectContaining({
allowIPAddressList: [],
allowMetaIPAddress: false,
allowPrivateIPAddress: false,
denyIPAddressList: [],
}),
}),
}),
);
});
});
});