Files
wehub-resource-sync 0d3cb498a3
CI / Shell Format Check (push) Has been cancelled
CI / Check Ruby (3.4) (push) Has been cancelled
CI / CI Config (push) Has been cancelled
CI / Test on Node ${{ matrix.node }} and ${{ matrix.os }}${{ matrix.shard && format(' (shard {0}/3)', matrix.shard) || '' }} (push) Has been cancelled
CI / Build on Node ${{ matrix.node }} (push) Has been cancelled
CI / Style Check (push) Has been cancelled
CI / Generate Assets (push) Has been cancelled
CI / Check Python (3.14) (push) Has been cancelled
CI / Check Python (3.9) (push) Has been cancelled
CI / Build Docs (push) Has been cancelled
CI / Code Scan Action (push) Has been cancelled
CI / Site tests (push) Has been cancelled
CI / webui tests (push) Has been cancelled
CI / Run Integration Tests (push) Has been cancelled
CI / Run Smoke Tests (push) Has been cancelled
CI / Go Tests (push) Has been cancelled
CI / Share Test (push) Has been cancelled
CI / Redteam (Production API) (push) Has been cancelled
CI / Redteam (Staging API) (push) Has been cancelled
CI / GitHub Actions Lint (push) Has been cancelled
CI / Check Ruby (3.0) (push) Has been cancelled
release-please / release-please (push) Has been cancelled
release-please / build (push) Has been cancelled
release-please / publish-npm (push) Has been cancelled
release-please / publish-npm-backfill (push) Has been cancelled
release-please / docker (push) Has been cancelled
release-please / publish-code-scan-action (push) Has been cancelled
release-please / attest-code-scan-action (push) Has been cancelled
Deploy local.promptfoo.app / Deploy to Cloudflare Pages (push) Has been cancelled
Test and Publish Multi-arch Docker Image / test (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-amd64 platform:linux/amd64 runner:ubuntu-latest]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / build-docker-and-push-digests (map[digest-suffix:linux-arm64 platform:linux/arm64 runner:ubuntu-24.04-arm]) (push) Has been cancelled
Test and Publish Multi-arch Docker Image / merge-docker-digests (push) Has been cancelled
Test and Publish Multi-arch Docker Image / Attest Multi-arch Image (push) Has been cancelled
Validate Renovate Config / Validate Renovate Configuration (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:24:08 +08:00

139 lines
6.1 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { ConfigurationError } from '../../../../src/commands/mcp/lib/errors';
import { validateFilePath, validateProviderId } from '../../../../src/commands/mcp/lib/security';
import { escapeRegExp } from '../../../../src/util/text';
describe('MCP Security', () => {
describe('validateFilePath', () => {
it('should allow simple relative paths', () => {
expect(() => validateFilePath('output.yaml')).not.toThrow();
expect(() => validateFilePath('data/results.json')).not.toThrow();
expect(() => validateFilePath('my-file.txt')).not.toThrow();
});
it('should reject paths containing ".."', () => {
expect(() => validateFilePath('../etc/passwd')).toThrow(ConfigurationError);
expect(() => validateFilePath('foo/../bar')).toThrow(ConfigurationError);
expect(() => validateFilePath('/tmp/../etc/passwd')).toThrow(ConfigurationError);
});
it('should reject paths containing "~"', () => {
expect(() => validateFilePath('~/.ssh/id_rsa')).toThrow(ConfigurationError);
expect(() => validateFilePath('~/Documents/file.txt')).toThrow(ConfigurationError);
});
it.skipIf(process.platform === 'win32')('should reject paths to system directories', () => {
// Unix paths are only recognized as absolute on Unix systems
expect(() => validateFilePath('/etc/passwd')).toThrow(ConfigurationError);
expect(() => validateFilePath('/sys/kernel')).toThrow(ConfigurationError);
expect(() => validateFilePath('/proc/self/environ')).toThrow(ConfigurationError);
expect(() => validateFilePath('/dev/null')).toThrow(ConfigurationError);
expect(() => validateFilePath('/var/run/docker.sock')).toThrow(ConfigurationError);
});
it.skipIf(process.platform !== 'win32')('should reject Windows system directories', () => {
// Windows paths are only recognized as absolute on Windows
expect(() => validateFilePath('C:\\Windows\\System32\\config')).toThrow(ConfigurationError);
expect(() => validateFilePath('C:\\Program Files\\app')).toThrow(ConfigurationError);
expect(() => validateFilePath('C:\\ProgramData\\secret')).toThrow(ConfigurationError);
});
it.skipIf(process.platform === 'win32')(
'should allow absolute paths to non-system directories',
() => {
// Unix paths are only recognized as absolute on Unix systems
expect(() => validateFilePath('/tmp/output.yaml')).not.toThrow();
expect(() => validateFilePath('/home/user/data.json')).not.toThrow();
expect(() => validateFilePath('/Users/test/file.txt')).not.toThrow();
},
);
describe('with basePath', () => {
it('should allow paths within the base directory', () => {
expect(() => validateFilePath('output.yaml', '/tmp/workdir')).not.toThrow();
expect(() => validateFilePath('subdir/file.txt', '/tmp/workdir')).not.toThrow();
});
it('should reject paths that escape the base directory', () => {
// Note: ".." is caught by the pre-normalization check
expect(() => validateFilePath('../escape.txt', '/tmp/workdir')).toThrow(ConfigurationError);
});
});
it('should include the original path in the error details', () => {
try {
validateFilePath('../etc/passwd');
expect.fail('Expected error to be thrown');
} catch (error) {
expect(error).toBeInstanceOf(ConfigurationError);
expect((error as ConfigurationError).details).toEqual({ configPath: '../etc/passwd' });
}
});
});
describe('escapeRegExp', () => {
it('should escape special regex characters', () => {
expect(escapeRegExp('hello.world')).toBe('hello\\.world');
expect(escapeRegExp('foo*bar')).toBe('foo\\*bar');
expect(escapeRegExp('a+b')).toBe('a\\+b');
expect(escapeRegExp('test?')).toBe('test\\?');
expect(escapeRegExp('^start')).toBe('\\^start');
expect(escapeRegExp('end$')).toBe('end\\$');
});
it('should escape brackets and braces', () => {
expect(escapeRegExp('[abc]')).toBe('\\[abc\\]');
expect(escapeRegExp('{1,3}')).toBe('\\{1,3\\}');
expect(escapeRegExp('(group)')).toBe('\\(group\\)');
});
it('should escape pipe and backslash', () => {
expect(escapeRegExp('a|b')).toBe('a\\|b');
expect(escapeRegExp('path\\to\\file')).toBe('path\\\\to\\\\file');
});
it('should return strings without special chars unchanged', () => {
expect(escapeRegExp('hello')).toBe('hello');
expect(escapeRegExp('openai:gpt-4')).toBe('openai:gpt-4');
expect(escapeRegExp('simple_test')).toBe('simple_test');
});
it('should handle empty strings', () => {
expect(escapeRegExp('')).toBe('');
});
it('should produce strings safe for regex construction', () => {
const userInput = 'openai:gpt-4.0';
const escaped = escapeRegExp(userInput);
const regex = new RegExp(escaped);
expect(regex.test('openai:gpt-4.0')).toBe(true);
expect(regex.test('openai:gpt-4X0')).toBe(false); // "." should not match any char
});
});
describe('validateProviderId', () => {
it('should accept valid provider:model format', () => {
expect(() => validateProviderId('openai:gpt-4')).not.toThrow();
expect(() => validateProviderId('anthropic:claude-3')).not.toThrow();
expect(() => validateProviderId('azure:gpt-4o')).not.toThrow();
});
it('should accept file path providers', () => {
expect(() => validateProviderId('providers/custom.js')).not.toThrow();
expect(() => validateProviderId('my-provider.ts')).not.toThrow();
expect(() => validateProviderId('script.py')).not.toThrow();
expect(() => validateProviderId('module.mjs')).not.toThrow();
});
it('should accept HTTP providers', () => {
expect(() => validateProviderId('http://localhost:8080/api')).not.toThrow();
expect(() => validateProviderId('https://api.example.com/v1')).not.toThrow();
});
it('should reject invalid formats', () => {
expect(() => validateProviderId('invalid')).toThrow(ConfigurationError);
expect(() => validateProviderId('')).toThrow(ConfigurationError);
});
});
});