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
139 lines
6.1 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|