Files
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

284 lines
10 KiB
TypeScript

/**
* SEC-001: Safe JSON Parsing Tests
*
* Verifies that the safe JSON parsing utilities properly protect against
* prototype pollution attacks via __proto__, constructor, and prototype keys.
*/
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { safeJsonParse, parseJsonOption, parseJsonFile } from '../../../src/cli/helpers/safe-json.js';
describe('safeJsonParse', () => {
describe('valid JSON parsing', () => {
it('parses simple objects', () => {
expect(safeJsonParse('{"a": 1}')).toEqual({ a: 1 });
});
it('parses nested objects', () => {
expect(safeJsonParse('{"a": {"b": {"c": 3}}}')).toEqual({ a: { b: { c: 3 } } });
});
it('parses arrays', () => {
expect(safeJsonParse('[1, 2, 3]')).toEqual([1, 2, 3]);
});
it('parses mixed types', () => {
expect(safeJsonParse('{"string": "test", "number": 42, "bool": true, "null": null}')).toEqual({
string: 'test',
number: 42,
bool: true,
null: null,
});
});
it('parses empty object', () => {
expect(safeJsonParse('{}')).toEqual({});
});
it('parses empty array', () => {
expect(safeJsonParse('[]')).toEqual([]);
});
});
describe('prototype pollution protection', () => {
// Store original prototype state
let originalPrototype: Record<string, unknown>;
beforeEach(() => {
// Capture current state of Object.prototype
originalPrototype = { ...Object.prototype };
});
afterEach(() => {
// Clean up any pollution that might have occurred (shouldn't happen with safe parsing)
const currentKeys = Object.keys(Object.prototype);
for (const key of currentKeys) {
if (!(key in originalPrototype)) {
delete (Object.prototype as Record<string, unknown>)[key];
}
}
});
it('removes __proto__ pollution attempts', () => {
const result = safeJsonParse('{"__proto__": {"polluted": true}}');
// The __proto__ key should be removed from the parsed object's own properties
expect(result).toEqual({});
expect(Object.prototype.hasOwnProperty.call(result, '__proto__')).toBe(false);
// Most importantly, Object.prototype should NOT be polluted
expect((Object.prototype as Record<string, unknown>).polluted).toBeUndefined();
expect(({} as Record<string, unknown>).polluted).toBeUndefined();
});
it('removes nested __proto__ pollution attempts', () => {
const result = safeJsonParse('{"a": {"__proto__": {"nested_polluted": true}}}');
// The nested object should exist but without __proto__
expect(result).toEqual({ a: {} });
// Object.prototype should NOT be polluted
expect((Object.prototype as Record<string, unknown>).nested_polluted).toBeUndefined();
});
it('removes constructor pollution attempts', () => {
const result = safeJsonParse('{"constructor": {"prototype": {"constructor_polluted": true}}}');
// The constructor key should be removed from parsed object's own properties
expect(Object.prototype.hasOwnProperty.call(result, 'constructor')).toBe(false);
// Object.prototype should NOT be polluted
expect((Object.prototype as Record<string, unknown>).constructor_polluted).toBeUndefined();
});
it('allows safe prototype string key (not dangerous)', () => {
// Note: A "prototype" string key is NOT dangerous - only __proto__ and constructor.prototype are
// The "prototype" key as a regular property doesn't affect Object.prototype
const result = safeJsonParse('{"a": {"prototype": {"value": true}}}');
// Regular "prototype" string keys are preserved (they're not dangerous)
expect(result).toEqual({ a: { prototype: { value: true } } });
// Verify no pollution occurred
expect((Object.prototype as Record<string, unknown>).value).toBeUndefined();
});
it('handles multiple pollution vectors in one payload', () => {
const maliciousPayload = JSON.stringify({
legitimate: 'data',
__proto__: { polluted1: true },
constructor: { prototype: { polluted2: true } },
nested: {
__proto__: { polluted3: true },
safe: 'value',
},
});
const result = safeJsonParse(maliciousPayload);
// Only legitimate keys should remain
expect(result).toEqual({
legitimate: 'data',
nested: {
safe: 'value',
},
});
// None of the pollution should have taken effect
expect((Object.prototype as Record<string, unknown>).polluted1).toBeUndefined();
expect((Object.prototype as Record<string, unknown>).polluted2).toBeUndefined();
expect((Object.prototype as Record<string, unknown>).polluted3).toBeUndefined();
});
it('safely handles deeply nested pollution attempts', () => {
const result = safeJsonParse('{"a": {"b": {"c": {"__proto__": {"deep_polluted": true}}}}}');
expect(result).toEqual({ a: { b: { c: {} } } });
expect((Object.prototype as Record<string, unknown>).deep_polluted).toBeUndefined();
});
});
describe('error handling', () => {
it('throws on invalid JSON', () => {
expect(() => safeJsonParse('not json')).toThrow();
});
it('throws on empty string', () => {
expect(() => safeJsonParse('')).toThrow();
});
it('throws on incomplete JSON', () => {
expect(() => safeJsonParse('{"incomplete":')).toThrow();
});
it('throws on trailing commas', () => {
expect(() => safeJsonParse('{"a": 1,}')).toThrow();
});
});
});
describe('parseJsonOption', () => {
it('parses valid JSON and returns typed result', () => {
const result = parseJsonOption<{ key: string }>('{"key": "value"}', 'test-option');
expect(result).toEqual({ key: 'value' });
});
it('removes prototype pollution attempts', () => {
const result = parseJsonOption('{"__proto__": {"polluted": true}, "safe": "data"}', 'params');
expect(result).toEqual({ safe: 'data' });
expect((Object.prototype as Record<string, unknown>).polluted).toBeUndefined();
});
it('throws with user-friendly error message on invalid JSON', () => {
expect(() => parseJsonOption('invalid json', 'params')).toThrow('Invalid JSON in --params:');
});
it('includes the option name in error message', () => {
expect(() => parseJsonOption('bad', 'payload')).toThrow('Invalid JSON in --payload:');
});
it('handles empty object JSON', () => {
expect(parseJsonOption('{}', 'params')).toEqual({});
});
});
describe('parseJsonFile', () => {
it('parses valid JSON file content', () => {
const content = '{"version": "1.0.0", "settings": {"debug": true}}';
const result = parseJsonFile(content, '/path/to/config.json');
expect(result).toEqual({ version: '1.0.0', settings: { debug: true } });
});
it('removes prototype pollution from config files', () => {
const maliciousConfig = '{"__proto__": {"isAdmin": true}, "name": "config"}';
const result = parseJsonFile(maliciousConfig, '/path/to/malicious.json');
expect(result).toEqual({ name: 'config' });
expect((Object.prototype as Record<string, unknown>).isAdmin).toBeUndefined();
});
it('throws with file path in error message', () => {
expect(() => parseJsonFile('invalid', '/etc/config.json')).toThrow(
'Invalid JSON in file /etc/config.json:'
);
});
it('handles complex nested structures', () => {
const content = JSON.stringify({
kernel: { eventBus: 'in-memory' },
domains: {
'test-generation': { enabled: true },
'coverage-analysis': { algorithm: 'hnsw' },
},
});
const result = parseJsonFile(content, 'config.json');
expect(result).toHaveProperty('kernel.eventBus', 'in-memory');
expect(result).toHaveProperty('domains.coverage-analysis.algorithm', 'hnsw');
});
});
describe('SEC-001 real-world attack scenarios', () => {
afterEach(() => {
// Extra safety: ensure Object.prototype is clean
const dangerousKeys = ['isAdmin', 'isAuthenticated', 'role', 'permissions', 'polluted'];
for (const key of dangerousKeys) {
if (key in Object.prototype) {
delete (Object.prototype as Record<string, unknown>)[key];
}
}
});
it('blocks privilege escalation via __proto__.isAdmin', () => {
const attackPayload = '{"username": "attacker", "__proto__": {"isAdmin": true}}';
const user = safeJsonParse(attackPayload);
// Verify the attack was blocked
expect((user as Record<string, unknown>).isAdmin).toBeUndefined();
expect(({} as Record<string, unknown>).isAdmin).toBeUndefined();
// New objects should not have isAdmin
const newObj: Record<string, unknown> = {};
expect(newObj.isAdmin).toBeUndefined();
});
it('blocks authentication bypass via constructor pollution', () => {
const attackPayload =
'{"token": "abc", "constructor": {"prototype": {"isAuthenticated": true}}}';
const session = safeJsonParse(attackPayload);
expect((session as Record<string, unknown>).isAuthenticated).toBeUndefined();
expect(({} as Record<string, unknown>).isAuthenticated).toBeUndefined();
});
it('blocks CLI parameter injection attacks', () => {
// Simulating a CLI command like: --params '{"__proto__": {"shell": "/bin/sh"}}'
const attackParams = '{"command": "build", "__proto__": {"shell": "/bin/sh"}}';
const params = parseJsonOption(attackParams, 'params');
expect(params).toEqual({ command: 'build' });
expect((params as Record<string, unknown>).shell).toBeUndefined();
expect(({} as Record<string, unknown>).shell).toBeUndefined();
});
it('blocks config file poisoning', () => {
// Malicious config file trying to inject permissions
const poisonedConfig = JSON.stringify({
name: 'my-project',
version: '1.0.0',
__proto__: {
permissions: ['read', 'write', 'admin'],
role: 'superuser',
},
});
const config = parseJsonFile(poisonedConfig, 'package.json');
expect(config).toEqual({ name: 'my-project', version: '1.0.0' });
expect((config as Record<string, unknown>).permissions).toBeUndefined();
expect((config as Record<string, unknown>).role).toBeUndefined();
expect(({} as Record<string, unknown>).permissions).toBeUndefined();
expect(({} as Record<string, unknown>).role).toBeUndefined();
});
});