Files
proffesor-for-testing__agen…/tests/unit/shared/io/file-reader-security.test.ts
Profa 2253d05618 feat: flatten project structure — archive v2, promote v3 to root
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>
2026-03-01 11:35:41 +00:00

283 lines
9.1 KiB
TypeScript

/**
* SEC-004: FileReader Path Traversal Protection Tests
*
* These tests verify that the FileReader properly blocks path traversal attacks
* using the validatePath security utility from cve-prevention.ts
*/
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import {
FileReader,
PathTraversalError,
FileReadError,
} from '../../../../src/shared/io/file-reader';
describe('FileReader path traversal protection (SEC-004)', () => {
let reader: FileReader;
const safeBasePath = '/safe/base/directory';
beforeEach(() => {
reader = new FileReader({ basePath: safeBasePath });
});
describe('readFile() path traversal protection', () => {
it('blocks basic ../ traversal attempts', async () => {
const result = await reader.readFile('../../../etc/passwd');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
expect((result.error as PathTraversalError).riskLevel).toBe('critical');
}
});
it('blocks URL-encoded ../ traversal (%2e%2e%2f)', async () => {
const result = await reader.readFile('%2e%2e%2f%2e%2e%2fetc/passwd');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
it('blocks double URL-encoded traversal (%252e%252e)', async () => {
const result = await reader.readFile('%252e%252e%252f%252e%252e%252fetc/passwd');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
it('blocks null byte injection attempts', async () => {
const result = await reader.readFile('file.txt\0.jpg');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
it('blocks URL-encoded null byte (%00)', async () => {
const result = await reader.readFile('file.txt%00.jpg');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
it('blocks Windows-style backslash traversal', async () => {
const result = await reader.readFile('..\\..\\..\\etc\\passwd');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
it('blocks access to /etc system paths', async () => {
const result = await reader.readFile('/etc/passwd');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
expect((result.error as PathTraversalError).riskLevel).toBe('critical');
}
});
it('blocks access to /proc system paths', async () => {
const result = await reader.readFile('/proc/self/environ');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
it('blocks dangerous file extensions (.exe)', async () => {
const result = await reader.readFile('malicious.exe');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
expect((result.error as PathTraversalError).riskLevel).toBe('high');
}
});
it('blocks dangerous file extensions (.dll)', async () => {
const result = await reader.readFile('library.dll');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
});
describe('readJSON() path traversal protection', () => {
it('blocks traversal attempts in JSON file paths', async () => {
const result = await reader.readJSON('../../../etc/shadow');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
});
describe('fileExists() path traversal protection', () => {
it('blocks traversal attempts in file existence checks', async () => {
const result = await reader.fileExists('../../../etc/passwd');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
});
describe('listFiles() path traversal protection', () => {
it('blocks traversal attempts in base path parameter', async () => {
const result = await reader.listFiles('*.txt', '../../../etc');
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
});
describe('invalidateCache() path traversal protection', () => {
it('throws PathTraversalError for malicious paths', () => {
expect(() => {
reader.invalidateCache('../../../etc/passwd');
}).toThrow(PathTraversalError);
});
});
describe('PathTraversalError properties', () => {
it('contains the requested path', async () => {
const maliciousPath = '../../../etc/passwd';
const result = await reader.readFile(maliciousPath);
expect(result.success).toBe(false);
if (!result.success && result.error instanceof PathTraversalError) {
expect(result.error.requestedPath).toBe(maliciousPath);
expect(result.error.issues.length).toBeGreaterThan(0);
expect(result.error.name).toBe('PathTraversalError');
}
});
it('has correct risk level for critical traversal', async () => {
const result = await reader.readFile('../../../etc/passwd');
expect(result.success).toBe(false);
if (!result.success && result.error instanceof PathTraversalError) {
expect(result.error.riskLevel).toBe('critical');
}
});
});
});
describe('FileReader valid path handling (SEC-004 regression)', () => {
let reader: FileReader;
let tempDir: string;
let tempFile: string;
beforeEach(async () => {
// Create a real temporary directory for testing valid paths
// Use a path that won't trigger system path checks
tempDir = path.join(process.cwd(), '.test-temp-' + Date.now());
await fs.mkdir(tempDir, { recursive: true });
tempFile = path.join(tempDir, 'valid-file.txt');
await fs.writeFile(tempFile, 'test content');
reader = new FileReader({ basePath: tempDir });
});
afterEach(async () => {
// Clean up temp directory
try {
await fs.rm(tempDir, { recursive: true, force: true });
} catch {
// Ignore cleanup errors
}
});
it('allows valid relative paths within basePath', async () => {
const result = await reader.readFile('valid-file.txt');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe('test content');
}
});
it('allows valid subdirectory paths', async () => {
// Create subdirectory with file
const subDir = path.join(tempDir, 'subdir');
await fs.mkdir(subDir, { recursive: true });
await fs.writeFile(path.join(subDir, 'nested.txt'), 'nested content');
const result = await reader.readFile('subdir/nested.txt');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value).toBe('nested content');
}
});
it('blocks absolute paths that escape basePath', async () => {
// Absolute paths to system directories should be blocked
const absoluteReader = new FileReader({ basePath: tempDir });
const result = await absoluteReader.readFile('/etc/passwd');
// Should be blocked as it escapes basePath
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error).toBeInstanceOf(PathTraversalError);
}
});
it('allows common source code extensions (.ts, .js)', async () => {
const tsFile = path.join(tempDir, 'module.ts');
await fs.writeFile(tsFile, 'export const x = 1;');
const result = await reader.readFile('module.ts');
expect(result.success).toBe(true);
});
it('allows JSON files with relative paths', async () => {
const jsonFile = path.join(tempDir, 'config.json');
await fs.writeFile(jsonFile, '{"key": "value"}');
// Use relative path which will be resolved correctly
const result = await reader.readJSON<{ key: string }>('config.json');
expect(result.success).toBe(true);
if (result.success) {
expect(result.value.key).toBe('value');
}
});
it('allows reading files that exist with valid paths', async () => {
// Test that valid file operations work
const existsResult = await reader.fileExists('valid-file.txt');
expect(existsResult.success).toBe(true);
if (existsResult.success) {
expect(existsResult.value).toBe(true);
}
// Test non-existent file returns false (not error)
const notExistsResult = await reader.fileExists('nonexistent.txt');
expect(notExistsResult.success).toBe(true);
if (notExistsResult.success) {
expect(notExistsResult.value).toBe(false);
}
});
});