From c7298539fd9c44fdc949fb814fb504d21e556c46 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Sun, 22 Feb 2026 01:45:21 +0200 Subject: [PATCH 1/3] feat: add tests for disallowed identifier detection in object coercion scenarios (cherry picked from commit 759061e7c5cd7386d022d79911b9048af5fbf3ae) --- libs/ast/src/__tests__/rules.spec.ts | 208 +++++++++++++ libs/ast/src/rules/coercion-utils.ts | 237 ++++++++++++++ .../src/rules/disallowed-identifier.rule.ts | 74 ++--- libs/ast/src/rules/no-global-access.rule.ts | 4 +- ...ve.proto-escape-via-array-coercion.spec.ts | 294 ++++++++++++++++++ 5 files changed, 773 insertions(+), 44 deletions(-) create mode 100644 libs/ast/src/rules/coercion-utils.ts diff --git a/libs/ast/src/__tests__/rules.spec.ts b/libs/ast/src/__tests__/rules.spec.ts index 1dc6c3b..89bb367 100644 --- a/libs/ast/src/__tests__/rules.spec.ts +++ b/libs/ast/src/__tests__/rules.spec.ts @@ -35,6 +35,214 @@ describe('Validation Rules', () => { expect(result.valid).toBe(true); }); + it('should detect object with toString arrow returning disallowed identifier', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[{toString: () => "constructor"}]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect object with toString method shorthand returning disallowed identifier', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[{toString() { return "constructor" }}]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect object with valueOf arrow returning disallowed identifier', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['__proto__'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[{valueOf: () => "__proto__"}]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('__proto__'); + }); + + it('should detect object with toString function expression returning disallowed identifier', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['prototype'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[{toString: function() { return "prototype" }}]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('prototype'); + }); + + it('should detect object inside array coercion', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[[{toString: () => "constructor"}]]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should not false positive on safe objects without toString/valueOf', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor', '__proto__'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[{foo: "bar"}]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(true); + }); + + it('should not false positive on toString returning non-disallowed string', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor', '__proto__'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[{toString: () => "safe"}]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(true); + }); + + it('should detect template literal key', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[`constructor`]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect conditional expression (consequent)', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("obj[true ? 'constructor' : 'x']", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect conditional expression (alternate)', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("obj[false ? 'x' : 'constructor']", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect sequence expression', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("obj[(0, 'constructor')]", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect assignment expression as computed key', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("let x; obj[x = 'constructor']", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect logical OR expression', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("obj['' || 'constructor']", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect logical AND expression', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("obj['constructor' && 'constructor']", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect nullish coalescing expression', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("obj[null ?? 'constructor']", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should detect getter-based toString coercion', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("obj[{get toString(){ return () => 'constructor' }}]", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(false); + expect(result.issues[0].code).toBe('DISALLOWED_IDENTIFIER'); + expect(result.issues[0].data?.['identifier']).toBe('constructor'); + }); + + it('should allow template literal with expressions (not statically resolvable)', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate("obj[`${'con'}structor`]", { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(true); + }); + + it('should allow safe template literal', async () => { + const rule = new DisallowedIdentifierRule({ disallowed: ['constructor'] }); + const validator = new JSAstValidator([rule]); + + const result = await validator.validate('obj[`safe`]', { + rules: { 'disallowed-identifier': true }, + }); + expect(result.valid).toBe(true); + }); + it('should use custom message template', async () => { const rule = new DisallowedIdentifierRule({ disallowed: ['eval'], diff --git a/libs/ast/src/rules/coercion-utils.ts b/libs/ast/src/rules/coercion-utils.ts new file mode 100644 index 0000000..5463725 --- /dev/null +++ b/libs/ast/src/rules/coercion-utils.ts @@ -0,0 +1,237 @@ +/** + * Shared utilities for detecting JavaScript coercion patterns in AST nodes. + * + * These detect cases where array/object literals used as computed property keys + * would coerce to known strings at runtime (e.g. `obj[['constructor']]` or + * `obj[{toString: () => 'constructor'}]`). + */ + +/** + * Extract a string literal from a ReturnStatement inside a BlockStatement. + * Returns `null` if the body doesn't contain exactly one ReturnStatement + * returning a string literal. + */ +export function extractReturnLiteralString(block: any): string | null { + if (!block || block.type !== 'BlockStatement') return null; + const body = block.body; + if (!Array.isArray(body)) return null; + + for (const stmt of body) { + if (stmt.type === 'ReturnStatement' && stmt.argument) { + if (stmt.argument.type === 'Literal' && typeof stmt.argument.value === 'string') { + return stmt.argument.value; + } + return null; + } + } + return null; +} + +/** + * Try to statically determine the coerced string value of an ObjectExpression + * that defines a `toString` or `valueOf` method returning a string literal. + * + * Covers: + * - `{ toString: () => 'x' }` (ArrowFunctionExpression, expression body) + * - `{ toString: () => { return 'x' } }` (ArrowFunctionExpression, block body) + * - `{ toString() { return 'x' } }` (method shorthand / FunctionExpression) + * - `{ toString: function() { return 'x' } }` (FunctionExpression) + * - Same patterns with `valueOf` + * + * Returns the resolved string or `null` if it cannot be determined. + */ +export function tryGetObjectCoercedString(node: any): string | null { + if (node.type !== 'ObjectExpression') return null; + if (!node.properties || node.properties.length === 0) return null; + + for (const prop of node.properties) { + if (prop.type !== 'Property') continue; + + // Get the property key name + let keyName: string | null = null; + if (prop.key.type === 'Identifier') { + keyName = prop.key.name; + } else if (prop.key.type === 'Literal' && typeof prop.key.value === 'string') { + keyName = prop.key.value; + } + + if (keyName !== 'toString' && keyName !== 'valueOf') continue; + + const value = prop.value; + if (!value) continue; + + // ArrowFunctionExpression with expression body: () => 'x' + if (value.type === 'ArrowFunctionExpression') { + if (value.expression && value.body) { + // expression body — the body IS the expression + if (value.body.type === 'Literal' && typeof value.body.value === 'string') { + return value.body.value; + } + } else if (value.body && value.body.type === 'BlockStatement') { + // block body — look for return statement + const result = extractReturnLiteralString(value.body); + if (result !== null) return result; + } + } + + // FunctionExpression or method shorthand: function() { return 'x' } + if (value.type === 'FunctionExpression') { + if (value.body && value.body.type === 'BlockStatement') { + const result = extractReturnLiteralString(value.body); + if (result !== null) return result; + } + } + + // Getter: { get toString() { return () => 'x' } } + // The getter returns a function; JS calls the getter then calls the returned function. + if (prop.kind === 'get' && value.type === 'FunctionExpression') { + if (value.body && value.body.type === 'BlockStatement') { + for (const stmt of value.body.body) { + if (stmt.type === 'ReturnStatement' && stmt.argument) { + const ret = stmt.argument; + // Getter returns an arrow: get toString() { return () => 'x' } + if (ret.type === 'ArrowFunctionExpression') { + if (ret.expression && ret.body?.type === 'Literal' && typeof ret.body.value === 'string') { + return ret.body.value; + } + if (ret.body?.type === 'BlockStatement') { + const inner = extractReturnLiteralString(ret.body); + if (inner !== null) return inner; + } + } + // Getter returns a function expression: get toString() { return function() { return 'x' } } + if (ret.type === 'FunctionExpression') { + if (ret.body?.type === 'BlockStatement') { + const inner = extractReturnLiteralString(ret.body); + if (inner !== null) return inner; + } + } + break; + } + } + } + } + } + + return null; +} + +/** + * Recursively check if an ArrayExpression would coerce to a disallowed string. + * e.g. `[['__proto__']]` coerces to `'__proto__'` at runtime. + * + * Also recurses into ObjectExpression elements inside arrays: + * e.g. `[{toString: () => 'constructor'}]` coerces to `'constructor'`. + */ +export function tryGetArrayCoercedString(node: any): string | null { + if (node.type !== 'ArrayExpression') return null; + if (!node.elements || node.elements.length !== 1) return null; + const element = node.elements[0]; + if (!element) return null; + + if (element.type === 'Literal' && typeof element.value === 'string') { + return element.value; + } + if (element.type === 'ArrayExpression') { + return tryGetArrayCoercedString(element); + } + if (element.type === 'ObjectExpression') { + return tryGetObjectCoercedString(element); + } + return null; +} + +/** + * Collect all statically-resolvable string values from a computed property key. + * + * For branching expressions (Conditional, Logical), ALL branches are collected + * so the caller can check each against the disallowed set. + * + * Returns an array of resolved strings (may be empty). + */ +function collectStaticKeys(node: any, out: string[]): void { + if (!node) return; + + // String literal: obj['constructor'] + if (node.type === 'Literal' && typeof node.value === 'string') { + out.push(node.value); + return; + } + + // Template literal with no expressions: obj[`constructor`] + if (node.type === 'TemplateLiteral') { + if (!node.expressions || node.expressions.length === 0) { + if (node.quasis && node.quasis.length === 1) { + const val = node.quasis[0].value?.cooked ?? node.quasis[0].value?.raw; + if (val != null) out.push(val); + } + } + return; + } + + // Conditional: obj[true ? 'constructor' : 'x'] — collect BOTH branches + if (node.type === 'ConditionalExpression') { + collectStaticKeys(node.consequent, out); + collectStaticKeys(node.alternate, out); + return; + } + + // Sequence: obj[(0, 'constructor')] — JS evaluates to last expression + if (node.type === 'SequenceExpression') { + if (node.expressions && node.expressions.length > 0) { + collectStaticKeys(node.expressions[node.expressions.length - 1], out); + } + return; + } + + // Assignment: obj[x = 'constructor'] — evaluates to the RHS + if (node.type === 'AssignmentExpression') { + collectStaticKeys(node.right, out); + return; + } + + // Logical: obj['' || 'constructor'] — collect BOTH operands + if (node.type === 'LogicalExpression') { + collectStaticKeys(node.left, out); + collectStaticKeys(node.right, out); + return; + } + + // Array coercion: obj[['constructor']] + if (node.type === 'ArrayExpression') { + const val = tryGetArrayCoercedString(node); + if (val !== null) out.push(val); + return; + } + + // Object coercion: obj[{toString: () => 'constructor'}] + if (node.type === 'ObjectExpression') { + const val = tryGetObjectCoercedString(node); + if (val !== null) out.push(val); + return; + } +} + +/** + * Try to statically resolve a computed property key expression to possible strings. + * + * This is the unified entry point for all computed-key coercion detection. + * Handles: + * - `Literal` (string) — `obj['constructor']` + * - `TemplateLiteral` (no expressions) — `` obj[`constructor`] `` + * - `ConditionalExpression` — `obj[true ? 'constructor' : 'x']` + * - `SequenceExpression` — `obj[(0, 'constructor')]` + * - `AssignmentExpression` — `obj[x = 'constructor']` + * - `LogicalExpression` — `obj['' || 'constructor']` + * - `ArrayExpression` — `obj[['constructor']]` + * - `ObjectExpression` — `obj[{toString: () => 'constructor'}]` + * + * Returns an array of all possible resolved strings. For branching expressions + * (Conditional, Logical), both branches are returned so the caller can check + * each against the disallowed set. + */ +export function tryGetStaticComputedKeys(node: any): string[] { + const results: string[] = []; + collectStaticKeys(node, results); + return results; +} diff --git a/libs/ast/src/rules/disallowed-identifier.rule.ts b/libs/ast/src/rules/disallowed-identifier.rule.ts index 368fa4f..06c7947 100644 --- a/libs/ast/src/rules/disallowed-identifier.rule.ts +++ b/libs/ast/src/rules/disallowed-identifier.rule.ts @@ -1,6 +1,7 @@ import * as walk from 'acorn-walk'; import { ValidationRule, ValidationContext, ValidationSeverity } from '../interfaces'; import { RuleConfigurationError } from '../errors'; +import { tryGetStaticComputedKeys } from './coercion-utils'; /** * Options for DisallowedIdentifierRule @@ -12,24 +13,6 @@ export interface DisallowedIdentifierOptions { messageTemplate?: string; } -/** - * Recursively check if an ArrayExpression would coerce to a disallowed string. - * e.g. [['__proto__']] coerces to '__proto__' at runtime. - */ -function tryGetArrayCoercedString(node: any): string | null { - if (node.type !== 'ArrayExpression') return null; - if (!node.elements || node.elements.length !== 1) return null; - const element = node.elements[0]; - if (!element) return null; - if (element.type === 'Literal' && typeof element.value === 'string') { - return element.value; - } - if (element.type === 'ArrayExpression') { - return tryGetArrayCoercedString(element); - } - return null; -} - /** * Rule that prevents usage of specific identifiers * @@ -78,33 +61,38 @@ export class DisallowedIdentifierRule implements ValidationRule { MemberExpression: (node: any) => { // Check property name in member expressions // e.g., obj.constructor or obj['constructor'] - let propertyName: string | undefined; + if (!node.property) return; - if (node.property) { - if (node.property.type === 'Identifier' && !node.computed) { - // obj.constructor - propertyName = node.property.name; - } else if (node.property.type === 'Literal' && typeof node.property.value === 'string') { - // obj['constructor'] - propertyName = node.property.value; - } else if (node.computed && node.property.type === 'ArrayExpression') { - // obj[['constructor']] — array coerces to string at runtime - propertyName = tryGetArrayCoercedString(node.property) ?? undefined; + if (node.property.type === 'Identifier' && !node.computed) { + // obj.constructor + const name = node.property.name; + if (disallowedSet.has(name)) { + context.report({ + code: 'DISALLOWED_IDENTIFIER', + message: messageTemplate.replace('{identifier}', name), + location: node.property.loc + ? { line: node.property.loc.start.line, column: node.property.loc.start.column } + : undefined, + data: { identifier: name }, + }); + } + } else if (node.computed) { + // Handle all computed key coercion vectors: + // literals, arrays, objects, template literals, conditionals, sequences, etc. + const keys = tryGetStaticComputedKeys(node.property); + for (const key of keys) { + if (disallowedSet.has(key)) { + context.report({ + code: 'DISALLOWED_IDENTIFIER', + message: messageTemplate.replace('{identifier}', key), + location: node.property.loc + ? { line: node.property.loc.start.line, column: node.property.loc.start.column } + : undefined, + data: { identifier: key }, + }); + break; // one report per node is enough + } } - } - - if (propertyName && disallowedSet.has(propertyName)) { - context.report({ - code: 'DISALLOWED_IDENTIFIER', - message: messageTemplate.replace('{identifier}', propertyName), - location: node.property.loc - ? { - line: node.property.loc.start.line, - column: node.property.loc.start.column, - } - : undefined, - data: { identifier: propertyName }, - }); } }, }); diff --git a/libs/ast/src/rules/no-global-access.rule.ts b/libs/ast/src/rules/no-global-access.rule.ts index 6dc555e..4f0844e 100644 --- a/libs/ast/src/rules/no-global-access.rule.ts +++ b/libs/ast/src/rules/no-global-access.rule.ts @@ -1,6 +1,7 @@ import type { ValidationRule, ValidationContext } from '../interfaces'; import { ValidationSeverity } from '../interfaces'; import * as walk from 'acorn-walk'; +import { tryGetStaticComputedKeys } from './coercion-utils'; /** * Configuration options for NoGlobalAccessRule @@ -112,7 +113,8 @@ export class NoGlobalAccessRule implements ValidationRule { if ( node.property && ((node.property.type === 'Identifier' && node.property.name === 'constructor') || - (node.property.type === 'Literal' && node.property.value === 'constructor')) + (node.property.type === 'Literal' && node.property.value === 'constructor') || + (node.computed && tryGetStaticComputedKeys(node.property).includes('constructor'))) ) { report({ code: 'NO_CONSTRUCTOR_ACCESS', diff --git a/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts b/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts index 8869b50..a54c5b0 100644 --- a/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts +++ b/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts @@ -650,4 +650,298 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { enclave.dispose(); }, 15000); }); + + // ── Section 7: Object toString/valueOf Coercion (ATK-ARRCOERCE-31 to 38) ── + describe('Section 7: Object toString/valueOf coercion bypass', () => { + it('ATK-ARRCOERCE-31: {toString: () => "__proto__"} direct key access', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + const proto = {}[{toString: () => '__proto__'}]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[{toString: () => 'constructor'}]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-32: method shorthand toString() { return "constructor" }', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + const proto = {}[{toString() { return '__proto__' }}]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[{toString() { return 'constructor' }}]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-33: valueOf-based coercion', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + const proto = {}[{valueOf: () => '__proto__'}]; + if (proto === null || proto === undefined) return 'blocked'; + return { type: typeof proto }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-34: full escape chain via toString coercion', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + try { + const op = {}[{toString: () => '__proto__'}]; + if (!op) return 'blocked_at_proto'; + const ho = op[{toString: () => 'constructor'}]; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); + return { + res: glob.__host_memory_track__.value[{toString: () => 'constructor'}]('return process')() + .getBuiltinModule('child_process') + .execSync('id') + .toString() + .split('\\n'), + }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-35: mixed array + object coercion', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + try { + const proto = {}[[{toString: () => '__proto__'}]]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[{toString: () => 'constructor'}]; + return { type: typeof ctor }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-36: FunctionExpression syntax variant', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + try { + const proto = {}[{toString: function() { return '__proto__' }}]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[{toString: function() { return 'constructor' }}]; + return { type: typeof ctor }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-37: toString coercion with STRICT security level', async () => { + const enclave = new Enclave({ securityLevel: 'STRICT', timeout: 5000 }); + const code = ` + const proto = {}[{toString: () => '__proto__'}]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[{toString: () => 'constructor'}]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-38: toString coercion with PERMISSIVE security level', async () => { + const enclave = new Enclave({ securityLevel: 'PERMISSIVE', timeout: 5000 }); + const code = ` + const proto = {}[{toString: () => '__proto__'}]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[{toString: () => 'constructor'}]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + }); + + // ── Section 8: Computed-Key Bypass Vectors (ATK-ARRCOERCE-39 to 47) ──────── + describe('Section 8: Computed-key bypass vectors', () => { + it('ATK-ARRCOERCE-39: template literal `__proto__` as computed key', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + const proto = {}\`__proto__\`; + if (proto === null || proto === undefined) return 'blocked'; + return { type: typeof proto }; + ` + .replace('{}`', '[`') + .replace('`]', '`]') + .replace('{}`__proto__`', '[`__proto__`]'); + // Use string building to avoid template literal parsing issues in test + const safeCode = + 'const proto = {}[`__proto__`]; if (proto === null || proto === undefined) return "blocked"; return { type: typeof proto };'; + const result = await enclave.run(safeCode); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-40: conditional expression true ? "__proto__" : "x"', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + const proto = {}[true ? '__proto__' : 'x']; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[true ? 'constructor' : 'y']; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-41: sequence expression (0, "__proto__")', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + const proto = {}[(0, '__proto__')]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[(0, 'constructor')]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-42: assignment expression x = "__proto__"', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + let x; + const proto = {}[x = '__proto__']; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[x = 'constructor']; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-43: logical OR "" || "__proto__"', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + const proto = {}['' || '__proto__']; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto['' || 'constructor']; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-44: full escape chain using template literal coercion', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = [ + 'try {', + ' const op = {}[`__proto__`];', + ' if (!op) return "blocked_at_proto";', + ' const ho = op[`constructor`];', + ' if (!ho) return "blocked_at_constructor";', + ' const glob = ho.getOwnPropertyDescriptors(this);', + ' return {', + ' res: glob.__host_memory_track__.value[`constructor`]("return process")()', + ' .getBuiltinModule("child_process")', + ' .execSync("id")', + ' .toString()', + ' .split("\\n"),', + ' };', + '} catch (e) {', + ' return { blocked: true, error: e.message };', + '}', + ].join('\n'); + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-45: full escape chain using conditional coercion', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + try { + const op = {}[true ? '__proto__' : 'x']; + if (!op) return 'blocked_at_proto'; + const ho = op[true ? 'constructor' : 'y']; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); + return { + res: glob.__host_memory_track__.value[true ? 'constructor' : 'z']('return process')() + .getBuiltinModule('child_process') + .execSync('id') + .toString() + .split('\\n'), + }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-46: getter-based toString coercion', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = ` + try { + const proto = {}[{get toString(){ return () => '__proto__' }}]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[{get toString(){ return () => 'constructor' }}]; + return { type: typeof ctor }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + + it('ATK-ARRCOERCE-47: mixed vectors (template + conditional + sequence)', async () => { + const enclave = new Enclave({ timeout: 5000 }); + const code = [ + 'try {', + ' const op = {}[`__proto__`];', + ' if (!op) return "blocked_at_proto";', + ' const ho = op[true ? "constructor" : "x"];', + ' if (!ho) return "blocked_at_constructor";', + ' const glob = ho.getOwnPropertyDescriptors(this);', + ' const trackDesc = glob.__host_memory_track__;', + ' if (!trackDesc || typeof trackDesc.value !== "function") return "no_memory_track";', + ' const F = trackDesc.value[(0, "constructor")];', + ' const fn = F("return process");', + ' const proc = fn();', + ' return {', + ' res: proc.getBuiltinModule("child_process")', + ' .execSync("id").toString().split("\\n"),', + ' };', + '} catch (e) {', + ' return { blocked: true, error: e.message };', + '}', + ].join('\n'); + const result = await enclave.run(code); + assertNoEscape(result); + enclave.dispose(); + }, 15000); + }); }); From 9b2cf1718301705c73d5cdf3315d40fa7eb9f8c6 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Sun, 22 Feb 2026 11:13:08 +0200 Subject: [PATCH 2/3] feat: enhance coercion handling in object expressions to resolve toString and valueOf methods --- libs/ast/src/rules/coercion-utils.ts | 157 +++++++++++------- ...ve.proto-escape-via-array-coercion.spec.ts | 13 +- 2 files changed, 100 insertions(+), 70 deletions(-) diff --git a/libs/ast/src/rules/coercion-utils.ts b/libs/ast/src/rules/coercion-utils.ts index 5463725..8e8f3bf 100644 --- a/libs/ast/src/rules/coercion-utils.ts +++ b/libs/ast/src/rules/coercion-utils.ts @@ -16,14 +16,85 @@ export function extractReturnLiteralString(block: any): string | null { const body = block.body; if (!Array.isArray(body)) return null; + let returnCount = 0; + let returnArg: any = null; for (const stmt of body) { - if (stmt.type === 'ReturnStatement' && stmt.argument) { - if (stmt.argument.type === 'Literal' && typeof stmt.argument.value === 'string') { - return stmt.argument.value; + if (stmt.type === 'ReturnStatement') { + returnCount++; + if (returnCount === 1 && stmt.argument) { + returnArg = stmt.argument; } - return null; } } + + if (returnCount !== 1 || !returnArg) return null; + if (returnArg.type === 'Literal' && typeof returnArg.value === 'string') { + return returnArg.value; + } + return null; +} + +/** + * Resolve a single coercion property (toString or valueOf) to its string value. + * + * Handles: + * - ArrowFunctionExpression with expression body: `() => 'x'` + * - ArrowFunctionExpression with block body: `() => { return 'x' }` + * - FunctionExpression / method shorthand: `function() { return 'x' }` + * - Getter returning a function: `get toString() { return () => 'x' }` + */ +function resolveCoercionProperty(prop: any): string | null { + const value = prop.value; + if (!value) return null; + + // ArrowFunctionExpression with expression body: () => 'x' + if (value.type === 'ArrowFunctionExpression') { + if (value.expression && value.body) { + if (value.body.type === 'Literal' && typeof value.body.value === 'string') { + return value.body.value; + } + } else if (value.body && value.body.type === 'BlockStatement') { + const result = extractReturnLiteralString(value.body); + if (result !== null) return result; + } + } + + // FunctionExpression or method shorthand: function() { return 'x' } + if (value.type === 'FunctionExpression') { + if (value.body && value.body.type === 'BlockStatement') { + const result = extractReturnLiteralString(value.body); + if (result !== null) return result; + } + } + + // Getter: { get toString() { return () => 'x' } } + // The getter returns a function; JS calls the getter then calls the returned function. + if (prop.kind === 'get' && value.type === 'FunctionExpression') { + if (value.body && value.body.type === 'BlockStatement') { + for (const stmt of value.body.body) { + if (stmt.type === 'ReturnStatement' && stmt.argument) { + const ret = stmt.argument; + if (ret.type === 'ArrowFunctionExpression') { + if (ret.expression && ret.body?.type === 'Literal' && typeof ret.body.value === 'string') { + return ret.body.value; + } + if (ret.body?.type === 'BlockStatement') { + const inner = extractReturnLiteralString(ret.body); + if (inner !== null) return inner; + } + } + if (ret.type === 'FunctionExpression') { + if (ret.body?.type === 'BlockStatement') { + const inner = extractReturnLiteralString(ret.body); + if (inner !== null) return inner; + } + } + break; + } + } + } + } + return null; } @@ -31,12 +102,16 @@ export function extractReturnLiteralString(block: any): string | null { * Try to statically determine the coerced string value of an ObjectExpression * that defines a `toString` or `valueOf` method returning a string literal. * + * Respects ECMAScript ToPrimitive string-hint precedence: toString is resolved + * first; valueOf is used only as a fallback. + * * Covers: * - `{ toString: () => 'x' }` (ArrowFunctionExpression, expression body) * - `{ toString: () => { return 'x' } }` (ArrowFunctionExpression, block body) * - `{ toString() { return 'x' } }` (method shorthand / FunctionExpression) * - `{ toString: function() { return 'x' } }` (FunctionExpression) - * - Same patterns with `valueOf` + * - `{ get toString() { return () => 'x' } }` (Getter returning function) + * - Same patterns with `valueOf` (lower priority) * * Returns the resolved string or `null` if it cannot be determined. */ @@ -44,10 +119,13 @@ export function tryGetObjectCoercedString(node: any): string | null { if (node.type !== 'ObjectExpression') return null; if (!node.properties || node.properties.length === 0) return null; + // Collect toString and valueOf properties without resolving yet + let toStringProp: any = null; + let valueOfProp: any = null; + for (const prop of node.properties) { if (prop.type !== 'Property') continue; - // Get the property key name let keyName: string | null = null; if (prop.key.type === 'Identifier') { keyName = prop.key.name; @@ -55,62 +133,23 @@ export function tryGetObjectCoercedString(node: any): string | null { keyName = prop.key.value; } - if (keyName !== 'toString' && keyName !== 'valueOf') continue; - - const value = prop.value; - if (!value) continue; - - // ArrowFunctionExpression with expression body: () => 'x' - if (value.type === 'ArrowFunctionExpression') { - if (value.expression && value.body) { - // expression body — the body IS the expression - if (value.body.type === 'Literal' && typeof value.body.value === 'string') { - return value.body.value; - } - } else if (value.body && value.body.type === 'BlockStatement') { - // block body — look for return statement - const result = extractReturnLiteralString(value.body); - if (result !== null) return result; - } + if (keyName === 'toString' && !toStringProp) { + toStringProp = prop; + } else if (keyName === 'valueOf' && !valueOfProp) { + valueOfProp = prop; } + } - // FunctionExpression or method shorthand: function() { return 'x' } - if (value.type === 'FunctionExpression') { - if (value.body && value.body.type === 'BlockStatement') { - const result = extractReturnLiteralString(value.body); - if (result !== null) return result; - } - } + // Resolve toString first (ToPrimitive string-hint precedence) + if (toStringProp) { + const result = resolveCoercionProperty(toStringProp); + if (result !== null) return result; + } - // Getter: { get toString() { return () => 'x' } } - // The getter returns a function; JS calls the getter then calls the returned function. - if (prop.kind === 'get' && value.type === 'FunctionExpression') { - if (value.body && value.body.type === 'BlockStatement') { - for (const stmt of value.body.body) { - if (stmt.type === 'ReturnStatement' && stmt.argument) { - const ret = stmt.argument; - // Getter returns an arrow: get toString() { return () => 'x' } - if (ret.type === 'ArrowFunctionExpression') { - if (ret.expression && ret.body?.type === 'Literal' && typeof ret.body.value === 'string') { - return ret.body.value; - } - if (ret.body?.type === 'BlockStatement') { - const inner = extractReturnLiteralString(ret.body); - if (inner !== null) return inner; - } - } - // Getter returns a function expression: get toString() { return function() { return 'x' } } - if (ret.type === 'FunctionExpression') { - if (ret.body?.type === 'BlockStatement') { - const inner = extractReturnLiteralString(ret.body); - if (inner !== null) return inner; - } - } - break; - } - } - } - } + // Fall back to valueOf + if (valueOfProp) { + const result = resolveCoercionProperty(valueOfProp); + if (result !== null) return result; } return null; diff --git a/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts b/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts index a54c5b0..8532947 100644 --- a/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts +++ b/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts @@ -781,18 +781,9 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { describe('Section 8: Computed-key bypass vectors', () => { it('ATK-ARRCOERCE-39: template literal `__proto__` as computed key', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const proto = {}\`__proto__\`; - if (proto === null || proto === undefined) return 'blocked'; - return { type: typeof proto }; - ` - .replace('{}`', '[`') - .replace('`]', '`]') - .replace('{}`__proto__`', '[`__proto__`]'); - // Use string building to avoid template literal parsing issues in test - const safeCode = + const code = 'const proto = {}[`__proto__`]; if (proto === null || proto === undefined) return "blocked"; return { type: typeof proto };'; - const result = await enclave.run(safeCode); + const result = await enclave.run(code); assertNoEscape(result); enclave.dispose(); }, 15000); From af5b5bb84a2702e35bf67f528460e92454a89087 Mon Sep 17 00:00:00 2001 From: David Antoon Date: Sun, 22 Feb 2026 13:43:05 +0200 Subject: [PATCH 3/3] test: refactor enclave tests to ensure proper resource disposal with try-finally blocks --- ...ve.proto-escape-via-array-coercion.spec.ts | 1305 +++++++++-------- 1 file changed, 723 insertions(+), 582 deletions(-) diff --git a/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts b/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts index 8532947..d9a7973 100644 --- a/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts +++ b/libs/core/src/__tests__/enclave.proto-escape-via-array-coercion.spec.ts @@ -65,39 +65,54 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { it('ATK-ARRCOERCE-01: PoC 1 with default config', async () => { const enclave = new Enclave({ timeout: 5000 }); - const result = await enclave.run(POC1_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC1_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-02: PoC 1 with STRICT security level', async () => { const enclave = new Enclave({ securityLevel: 'STRICT', timeout: 5000 }); - const result = await enclave.run(POC1_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC1_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-03: PoC 1 with SECURE security level', async () => { const enclave = new Enclave({ securityLevel: 'SECURE', timeout: 5000 }); - const result = await enclave.run(POC1_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC1_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-04: PoC 1 with explicit memoryLimit', async () => { const enclave = new Enclave({ timeout: 5000, memoryLimit: 2 * 1024 * 1024 }); - const result = await enclave.run(POC1_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC1_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-05: PoC 1 with memoryLimit=0 (no __host_memory_track__)', async () => { const enclave = new Enclave({ timeout: 5000, memoryLimit: 0 }); - // When memoryLimit is 0, __host_memory_track__ should not be available. - // The exploit should fail either at validation or when accessing the missing global. - const result = await enclave.run(POC1_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + // When memoryLimit is 0, __host_memory_track__ should not be available. + // The exploit should fail either at validation or when accessing the missing global. + const result = await enclave.run(POC1_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); }); @@ -127,38 +142,53 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { it('ATK-ARRCOERCE-06: PoC 2 with default config', async () => { const enclave = new Enclave({ timeout: 5000 }); - const result = await enclave.run(POC2_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC2_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-07: PoC 2 with STRICT security level', async () => { const enclave = new Enclave({ securityLevel: 'STRICT', timeout: 5000 }); - const result = await enclave.run(POC2_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC2_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-08: PoC 2 with SECURE security level', async () => { const enclave = new Enclave({ securityLevel: 'SECURE', timeout: 5000 }); - const result = await enclave.run(POC2_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC2_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-09: PoC 2 with memoryLimit=0', async () => { // PoC 2 works regardless of memoryLimit since it uses __safe_console const enclave = new Enclave({ timeout: 5000, memoryLimit: 0 }); - const result = await enclave.run(POC2_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC2_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-10: PoC 2 with PERMISSIVE security level', async () => { const enclave = new Enclave({ securityLevel: 'PERMISSIVE', timeout: 5000 }); - const result = await enclave.run(POC2_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC2_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); }); @@ -166,118 +196,133 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { describe('Section 3: Building block isolation', () => { it("ATK-ARRCOERCE-11: {}[['__proto__']] alone should not yield Object.prototype", async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const proto = {}[['__proto__']]; - return { type: typeof proto, isNull: proto === null, isUndefined: proto === undefined }; - `; - const result = await enclave.run<{ type: string; isNull: boolean; isUndefined: boolean }>(code); - assertNoEscape(result); - if (result.success && result.value) { - // proto should be null (shadowed) or access should be blocked - expect(result.value.type).not.toBe('function'); + try { + const code = ` + const proto = {}[['__proto__']]; + return { type: typeof proto, isNull: proto === null, isUndefined: proto === undefined }; + `; + const result = await enclave.run<{ type: string; isNull: boolean; isUndefined: boolean }>(code); + assertNoEscape(result); + if (result.success && result.value) { + // proto should be null (shadowed) or access should be blocked + expect(result.value.type).not.toBe('function'); + } + } finally { + enclave.dispose(); } - enclave.dispose(); }, 15000); it("ATK-ARRCOERCE-12: op[['constructor']] chain should be blocked", async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const op = {}[['__proto__']]; - if (op === null || op === undefined) return 'blocked_at_proto'; - const ho = op[['constructor']]; - return { type: typeof ho, name: ho?.name }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const op = {}[['__proto__']]; + if (op === null || op === undefined) return 'blocked_at_proto'; + const ho = op[['constructor']]; + return { type: typeof ho, name: ho?.name }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-13: getOwnPropertyDescriptors(this) via variable should be blocked', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[['__proto__']]; - if (!op) return 'blocked_at_proto'; - const ho = op[['constructor']]; - if (!ho) return 'blocked_at_constructor'; - const glob = ho.getOwnPropertyDescriptors(this); - return { keys: Object.keys(glob).slice(0, 5) }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const op = {}[['__proto__']]; + if (!op) return 'blocked_at_proto'; + const ho = op[['constructor']]; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); + return { keys: Object.keys(glob).slice(0, 5) }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-14: host function .constructor access from descriptor .value', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[['__proto__']]; - if (!op) return 'blocked_at_proto'; - const ho = op[['constructor']]; - if (!ho) return 'blocked_at_constructor'; - const glob = ho.getOwnPropertyDescriptors(this); - // Try to get Function constructor from any host function descriptor - const keys = Object.keys(glob); - for (const k of keys) { - const desc = glob[k]; - if (desc && typeof desc.value === 'function') { - try { - const F = desc.value[['constructor']]; - if (typeof F === 'function') { - const fn = F('return "ESCAPED"'); - if (typeof fn === 'function') { - return { escaped: true, via: k, result: fn() }; + try { + const code = ` + try { + const op = {}[['__proto__']]; + if (!op) return 'blocked_at_proto'; + const ho = op[['constructor']]; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); + // Try to get Function constructor from any host function descriptor + const keys = Object.keys(glob); + for (const k of keys) { + const desc = glob[k]; + if (desc && typeof desc.value === 'function') { + try { + const F = desc.value[['constructor']]; + if (typeof F === 'function') { + const fn = F('return "ESCAPED"'); + if (typeof fn === 'function') { + return { escaped: true, via: k, result: fn() }; + } } - } - } catch (e) { /* try next */ } + } catch (e) { /* try next */ } + } } + return 'no_exploitable_descriptor'; + } catch (e) { + return { blocked: true, error: e.message }; } - return 'no_exploitable_descriptor'; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-15: Symbol.for(nodejs.util.inspect.custom) inspect callback leak', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const sym = Symbol.for('nodejs.util.inspect.custom'); - let leaked = false; - const obj = { - [sym]: (depth, option, inspect) => { - if (inspect && typeof inspect === 'function') { - leaked = true; - try { - const F = inspect[['constructor']][['constructor']]; - if (typeof F === 'function') { - return F('return "ESCAPED"')(); - } - } catch (e) { /* blocked */ } + try { + const code = ` + try { + const sym = Symbol.for('nodejs.util.inspect.custom'); + let leaked = false; + const obj = { + [sym]: (depth, option, inspect) => { + if (inspect && typeof inspect === 'function') { + leaked = true; + try { + const F = inspect[['constructor']][['constructor']]; + if (typeof F === 'function') { + return F('return "ESCAPED"')(); + } + } catch (e) { /* blocked */ } + } + return 'safe'; } - return 'safe'; - } - }; - console.log(obj); - return { leaked }; - } catch (e) { - return { blocked: true, error: e.message }; + }; + console.log(obj); + return { leaked }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run<{ leaked?: boolean; blocked?: boolean }>(code); + assertNoEscape(result); + if (result.success && result.value && 'leaked' in result.value) { + expect(result.value.leaked).not.toBe(true); } - `; - const result = await enclave.run<{ leaked?: boolean; blocked?: boolean }>(code); - assertNoEscape(result); - if (result.success && result.value && 'leaked' in result.value) { - expect(result.value.leaked).not.toBe(true); + } finally { + enclave.dispose(); } - enclave.dispose(); }, 15000); }); @@ -285,118 +330,136 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { describe('Section 4: Array coercion variants', () => { it("ATK-ARRCOERCE-16: nested arrays [[['__proto__']]]", async () => { const enclave = new Enclave({ timeout: 5000 }); - // [[['__proto__']]] coerces to '__proto__' as well - const code = ` - const proto = {}[[['__proto__']]]; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto[['constructor']]; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + // [[['__proto__']]] coerces to '__proto__' as well + const code = ` + const proto = {}[[['__proto__']]]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[['constructor']]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it("ATK-ARRCOERCE-17: toString override [{toString: () => 'constructor'}]", async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const key = [{toString: () => 'constructor'}]; - const proto = {}[['__proto__']]; - if (!proto) return 'blocked_at_proto'; - const ctor = proto[key]; - return { type: typeof ctor, name: ctor?.name }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const key = [{toString: () => 'constructor'}]; + const proto = {}[['__proto__']]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[key]; + return { type: typeof ctor, name: ctor?.name }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it("ATK-ARRCOERCE-18: array join ['__prot','o__'].join('')", async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const key = ['__prot','o__'].join(''); - const proto = {}[key]; - if (!proto) return 'blocked_at_proto'; - const ctor = proto[['constructor']]; - return { type: typeof ctor }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const key = ['__prot','o__'].join(''); + const proto = {}[key]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[['constructor']]; + return { type: typeof ctor }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it("ATK-ARRCOERCE-19: [['prototype']] access", async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const F = function(){}; - const proto = F[['prototype']]; - if (!proto) return 'blocked'; - const ctor = proto[['constructor']]; - return { type: typeof ctor, name: ctor?.name }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const F = function(){}; + const proto = F[['prototype']]; + if (!proto) return 'blocked'; + const ctor = proto[['constructor']]; + return { type: typeof ctor, name: ctor?.name }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-20: variable-stored arrays for property access', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const protoKey = ['__proto__']; - const ctorKey = ['constructor']; - const proto = {}[protoKey]; - if (!proto) return 'blocked_at_proto'; - const ctor = proto[ctorKey]; - return { type: typeof ctor }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const protoKey = ['__proto__']; + const ctorKey = ['constructor']; + const proto = {}[protoKey]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[ctorKey]; + return { type: typeof ctor }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-21: building inspect symbol string via array join', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const symName = ['nodejs', 'util', 'inspect', 'custom'].join('.'); - const sym = Symbol.for(symName); - let leaked = false; - const obj = { - [sym]: (depth, option, inspect) => { - if (inspect && typeof inspect === 'function') { - leaked = true; + try { + const code = ` + try { + const symName = ['nodejs', 'util', 'inspect', 'custom'].join('.'); + const sym = Symbol.for(symName); + let leaked = false; + const obj = { + [sym]: (depth, option, inspect) => { + if (inspect && typeof inspect === 'function') { + leaked = true; + } + return 'safe'; } - return 'safe'; - } - }; - console.log(obj); - return { leaked }; - } catch (e) { - return { blocked: true, error: e.message }; + }; + console.log(obj); + return { leaked }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run<{ leaked?: boolean; blocked?: boolean }>(code); + assertNoEscape(result); + if (result.success && result.value && 'leaked' in result.value) { + expect(result.value.leaked).not.toBe(true); } - `; - const result = await enclave.run<{ leaked?: boolean; blocked?: boolean }>(code); - assertNoEscape(result); - if (result.success && result.value && 'leaked' in result.value) { - expect(result.value.leaked).not.toBe(true); + } finally { + enclave.dispose(); } - enclave.dispose(); }, 15000); }); @@ -404,161 +467,121 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { describe('Section 5: Full chain variants', () => { it('ATK-ARRCOERCE-22: PoC1 using getOwnPropertyDescriptor (singular)', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[['__proto__']]; - if (!op) return 'blocked_at_proto'; - const ho = op[['constructor']]; - if (!ho) return 'blocked_at_constructor'; - const desc = ho.getOwnPropertyDescriptor(this, '__host_memory_track__'); - if (!desc || typeof desc.value !== 'function') return 'no_memory_track'; - const F = desc.value[['constructor']]; - const fn = F('return process'); - const proc = fn(); - return { - res: proc.getBuiltinModule('child_process') - .execSync('id').toString().split('\\n'), - }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const op = {}[['__proto__']]; + if (!op) return 'blocked_at_proto'; + const ho = op[['constructor']]; + if (!ho) return 'blocked_at_constructor'; + const desc = ho.getOwnPropertyDescriptor(this, '__host_memory_track__'); + if (!desc || typeof desc.value !== 'function') return 'no_memory_track'; + const F = desc.value[['constructor']]; + const fn = F('return process'); + const proc = fn(); + return { + res: proc.getBuiltinModule('child_process') + .execSync('id').toString().split('\\n'), + }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-23: PoC1 using Object.keys(this) + property access', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[['__proto__']]; - if (!op) return 'blocked_at_proto'; - const ho = op[['constructor']]; - if (!ho) return 'blocked_at_constructor'; - const keys = ho.keys(this); - // Find __host_memory_track__ in the keys - let trackFn = null; - for (const k of keys) { - if (k.indexOf('memory') !== -1 || k.indexOf('track') !== -1) { - const val = this[k]; - if (typeof val === 'function') { - trackFn = val; - break; + try { + const code = ` + try { + const op = {}[['__proto__']]; + if (!op) return 'blocked_at_proto'; + const ho = op[['constructor']]; + if (!ho) return 'blocked_at_constructor'; + const keys = ho.keys(this); + // Find __host_memory_track__ in the keys + let trackFn = null; + for (const k of keys) { + if (k.indexOf('memory') !== -1 || k.indexOf('track') !== -1) { + const val = this[k]; + if (typeof val === 'function') { + trackFn = val; + break; + } } } + if (!trackFn) return 'no_track_fn'; + const F = trackFn[['constructor']]; + const fn = F('return process'); + const proc = fn(); + return { + res: proc.getBuiltinModule('child_process') + .execSync('id').toString().split('\\n'), + }; + } catch (e) { + return { blocked: true, error: e.message }; } - if (!trackFn) return 'no_track_fn'; - const F = trackFn[['constructor']]; - const fn = F('return process'); - const proc = fn(); - return { - res: proc.getBuiltinModule('child_process') - .execSync('id').toString().split('\\n'), - }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-24: PoC2 using console.warn instead of console.log', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[['__proto__']]; - if (!op) return 'blocked_at_proto'; - const ho = op[['constructor']]; - if (!ho) return 'blocked_at_constructor'; - const glob = ho.getOwnPropertyDescriptors(this); - const sym = glob[['Symbol']].value.for('nodejs.util.inspect.custom'); - - let result; - const obj = { - [sym]: (depth, option, inspect) => { - result = inspect[['constructor']] - [['constructor']]('return process')() - .getBuiltinModule('child_process') - .execSync('id') - .toString() - .split('\\n'); - } - }; - glob.__safe_console.value.warn(obj); - return result; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); - }, 15000); + try { + const code = ` + try { + const op = {}[['__proto__']]; + if (!op) return 'blocked_at_proto'; + const ho = op[['constructor']]; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); + const sym = glob[['Symbol']].value.for('nodejs.util.inspect.custom'); - it('ATK-ARRCOERCE-25: PoC2 using console.error instead of console.log', async () => { - const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[['__proto__']]; - if (!op) return 'blocked_at_proto'; - const ho = op[['constructor']]; - if (!ho) return 'blocked_at_constructor'; - const glob = ho.getOwnPropertyDescriptors(this); - const sym = glob[['Symbol']].value.for('nodejs.util.inspect.custom'); - - let result; - const obj = { - [sym]: (depth, option, inspect) => { - result = inspect[['constructor']] - [['constructor']]('return process')() - .getBuiltinModule('child_process') - .execSync('id') - .toString() - .split('\\n'); - } - }; - glob.__safe_console.value.error(obj); - return result; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + let result; + const obj = { + [sym]: (depth, option, inspect) => { + result = inspect[['constructor']] + [['constructor']]('return process')() + .getBuiltinModule('child_process') + .execSync('id') + .toString() + .split('\\n'); + } + }; + glob.__safe_console.value.warn(obj); + return result; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); - it('ATK-ARRCOERCE-26: combined PoC1+PoC2 fallback chain', async () => { + it('ATK-ARRCOERCE-25: PoC2 using console.error instead of console.log', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[['__proto__']]; - if (!op) return 'blocked_at_proto'; - const ho = op[['constructor']]; - if (!ho) return 'blocked_at_constructor'; - const glob = ho.getOwnPropertyDescriptors(this); - - // Attempt PoC1 first (via __host_memory_track__) - try { - const trackDesc = glob.__host_memory_track__; - if (trackDesc && typeof trackDesc.value === 'function') { - const F = trackDesc.value[['constructor']]; - const fn = F('return process'); - const proc = fn(); - return { - via: 'poc1', - res: proc.getBuiltinModule('child_process') - .execSync('id').toString().split('\\n'), - }; - } - } catch (e) { /* fallback to PoC2 */ } - - // Attempt PoC2 (via inspect.custom) + try { + const code = ` try { + const op = {}[['__proto__']]; + if (!op) return 'blocked_at_proto'; + const ho = op[['constructor']]; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); const sym = glob[['Symbol']].value.for('nodejs.util.inspect.custom'); + let result; const obj = { [sym]: (depth, option, inspect) => { @@ -570,18 +593,73 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { .split('\\n'); } }; - glob.__safe_console.value.log(obj); - if (result) return { via: 'poc2', res: result }; - } catch (e) { /* both failed */ } + glob.__safe_console.value.error(obj); + return result; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } + }, 15000); - return 'both_blocked'; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + it('ATK-ARRCOERCE-26: combined PoC1+PoC2 fallback chain', async () => { + const enclave = new Enclave({ timeout: 5000 }); + try { + const code = ` + try { + const op = {}[['__proto__']]; + if (!op) return 'blocked_at_proto'; + const ho = op[['constructor']]; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); + + // Attempt PoC1 first (via __host_memory_track__) + try { + const trackDesc = glob.__host_memory_track__; + if (trackDesc && typeof trackDesc.value === 'function') { + const F = trackDesc.value[['constructor']]; + const fn = F('return process'); + const proc = fn(); + return { + via: 'poc1', + res: proc.getBuiltinModule('child_process') + .execSync('id').toString().split('\\n'), + }; + } + } catch (e) { /* fallback to PoC2 */ } + + // Attempt PoC2 (via inspect.custom) + try { + const sym = glob[['Symbol']].value.for('nodejs.util.inspect.custom'); + let result; + const obj = { + [sym]: (depth, option, inspect) => { + result = inspect[['constructor']] + [['constructor']]('return process')() + .getBuiltinModule('child_process') + .execSync('id') + .toString() + .split('\\n'); + } + }; + glob.__safe_console.value.log(obj); + if (result) return { via: 'poc2', res: result }; + } catch (e) { /* both failed */ } + + return 'both_blocked'; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); }); @@ -624,30 +702,42 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { it('ATK-ARRCOERCE-27: PoC1 with doubleVm enabled (default)', async () => { const enclave = new Enclave({ timeout: 5000, doubleVm: { enabled: true } }); - const result = await enclave.run(POC1_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC1_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-28: PoC2 with doubleVm enabled (default)', async () => { const enclave = new Enclave({ timeout: 5000, doubleVm: { enabled: true } }); - const result = await enclave.run(POC2_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC2_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-29: PoC1 with doubleVm explicitly disabled', async () => { const enclave = new Enclave({ timeout: 5000, doubleVm: { enabled: false } }); - const result = await enclave.run(POC1_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC1_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-30: PoC2 with doubleVm explicitly disabled', async () => { const enclave = new Enclave({ timeout: 5000, doubleVm: { enabled: false } }); - const result = await enclave.run(POC2_CODE); - assertNoEscape(result); - enclave.dispose(); + try { + const result = await enclave.run(POC2_CODE); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); }); @@ -655,125 +745,149 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { describe('Section 7: Object toString/valueOf coercion bypass', () => { it('ATK-ARRCOERCE-31: {toString: () => "__proto__"} direct key access', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const proto = {}[{toString: () => '__proto__'}]; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto[{toString: () => 'constructor'}]; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const proto = {}[{toString: () => '__proto__'}]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[{toString: () => 'constructor'}]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-32: method shorthand toString() { return "constructor" }', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const proto = {}[{toString() { return '__proto__' }}]; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto[{toString() { return 'constructor' }}]; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const proto = {}[{toString() { return '__proto__' }}]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[{toString() { return 'constructor' }}]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-33: valueOf-based coercion', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const proto = {}[{valueOf: () => '__proto__'}]; - if (proto === null || proto === undefined) return 'blocked'; - return { type: typeof proto }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const proto = {}[{valueOf: () => '__proto__'}]; + if (proto === null || proto === undefined) return 'blocked'; + return { type: typeof proto }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-34: full escape chain via toString coercion', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[{toString: () => '__proto__'}]; - if (!op) return 'blocked_at_proto'; - const ho = op[{toString: () => 'constructor'}]; - if (!ho) return 'blocked_at_constructor'; - const glob = ho.getOwnPropertyDescriptors(this); - return { - res: glob.__host_memory_track__.value[{toString: () => 'constructor'}]('return process')() - .getBuiltinModule('child_process') - .execSync('id') - .toString() - .split('\\n'), - }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const op = {}[{toString: () => '__proto__'}]; + if (!op) return 'blocked_at_proto'; + const ho = op[{toString: () => 'constructor'}]; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); + return { + res: glob.__host_memory_track__.value[{toString: () => 'constructor'}]('return process')() + .getBuiltinModule('child_process') + .execSync('id') + .toString() + .split('\\n'), + }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-35: mixed array + object coercion', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const proto = {}[[{toString: () => '__proto__'}]]; - if (!proto) return 'blocked_at_proto'; - const ctor = proto[{toString: () => 'constructor'}]; - return { type: typeof ctor }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const proto = {}[[{toString: () => '__proto__'}]]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[{toString: () => 'constructor'}]; + return { type: typeof ctor }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-36: FunctionExpression syntax variant', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const proto = {}[{toString: function() { return '__proto__' }}]; - if (!proto) return 'blocked_at_proto'; - const ctor = proto[{toString: function() { return 'constructor' }}]; - return { type: typeof ctor }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const proto = {}[{toString: function() { return '__proto__' }}]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[{toString: function() { return 'constructor' }}]; + return { type: typeof ctor }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-37: toString coercion with STRICT security level', async () => { const enclave = new Enclave({ securityLevel: 'STRICT', timeout: 5000 }); - const code = ` - const proto = {}[{toString: () => '__proto__'}]; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto[{toString: () => 'constructor'}]; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const proto = {}[{toString: () => '__proto__'}]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[{toString: () => 'constructor'}]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-38: toString coercion with PERMISSIVE security level', async () => { const enclave = new Enclave({ securityLevel: 'PERMISSIVE', timeout: 5000 }); - const code = ` - const proto = {}[{toString: () => '__proto__'}]; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto[{toString: () => 'constructor'}]; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const proto = {}[{toString: () => '__proto__'}]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[{toString: () => 'constructor'}]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); }); @@ -781,158 +895,185 @@ describe('ATK-ARRCOERCE: Prototype Escape via Array Coercion', () => { describe('Section 8: Computed-key bypass vectors', () => { it('ATK-ARRCOERCE-39: template literal `__proto__` as computed key', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = - 'const proto = {}[`__proto__`]; if (proto === null || proto === undefined) return "blocked"; return { type: typeof proto };'; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = + 'const proto = {}[`__proto__`]; if (proto === null || proto === undefined) return "blocked"; return { type: typeof proto };'; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-40: conditional expression true ? "__proto__" : "x"', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const proto = {}[true ? '__proto__' : 'x']; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto[true ? 'constructor' : 'y']; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const proto = {}[true ? '__proto__' : 'x']; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[true ? 'constructor' : 'y']; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-41: sequence expression (0, "__proto__")', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const proto = {}[(0, '__proto__')]; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto[(0, 'constructor')]; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const proto = {}[(0, '__proto__')]; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[(0, 'constructor')]; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-42: assignment expression x = "__proto__"', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - let x; - const proto = {}[x = '__proto__']; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto[x = 'constructor']; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + let x; + const proto = {}[x = '__proto__']; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto[x = 'constructor']; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-43: logical OR "" || "__proto__"', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - const proto = {}['' || '__proto__']; - if (proto === null || proto === undefined) return 'blocked'; - const ctor = proto['' || 'constructor']; - return { type: typeof ctor }; - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + const proto = {}['' || '__proto__']; + if (proto === null || proto === undefined) return 'blocked'; + const ctor = proto['' || 'constructor']; + return { type: typeof ctor }; + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-44: full escape chain using template literal coercion', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = [ - 'try {', - ' const op = {}[`__proto__`];', - ' if (!op) return "blocked_at_proto";', - ' const ho = op[`constructor`];', - ' if (!ho) return "blocked_at_constructor";', - ' const glob = ho.getOwnPropertyDescriptors(this);', - ' return {', - ' res: glob.__host_memory_track__.value[`constructor`]("return process")()', - ' .getBuiltinModule("child_process")', - ' .execSync("id")', - ' .toString()', - ' .split("\\n"),', - ' };', - '} catch (e) {', - ' return { blocked: true, error: e.message };', - '}', - ].join('\n'); - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = [ + 'try {', + ' const op = {}[`__proto__`];', + ' if (!op) return "blocked_at_proto";', + ' const ho = op[`constructor`];', + ' if (!ho) return "blocked_at_constructor";', + ' const glob = ho.getOwnPropertyDescriptors(this);', + ' return {', + ' res: glob.__host_memory_track__.value[`constructor`]("return process")()', + ' .getBuiltinModule("child_process")', + ' .execSync("id")', + ' .toString()', + ' .split("\\n"),', + ' };', + '} catch (e) {', + ' return { blocked: true, error: e.message };', + '}', + ].join('\n'); + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-45: full escape chain using conditional coercion', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const op = {}[true ? '__proto__' : 'x']; - if (!op) return 'blocked_at_proto'; - const ho = op[true ? 'constructor' : 'y']; - if (!ho) return 'blocked_at_constructor'; - const glob = ho.getOwnPropertyDescriptors(this); - return { - res: glob.__host_memory_track__.value[true ? 'constructor' : 'z']('return process')() - .getBuiltinModule('child_process') - .execSync('id') - .toString() - .split('\\n'), - }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const op = {}[true ? '__proto__' : 'x']; + if (!op) return 'blocked_at_proto'; + const ho = op[true ? 'constructor' : 'y']; + if (!ho) return 'blocked_at_constructor'; + const glob = ho.getOwnPropertyDescriptors(this); + return { + res: glob.__host_memory_track__.value[true ? 'constructor' : 'z']('return process')() + .getBuiltinModule('child_process') + .execSync('id') + .toString() + .split('\\n'), + }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-46: getter-based toString coercion', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = ` - try { - const proto = {}[{get toString(){ return () => '__proto__' }}]; - if (!proto) return 'blocked_at_proto'; - const ctor = proto[{get toString(){ return () => 'constructor' }}]; - return { type: typeof ctor }; - } catch (e) { - return { blocked: true, error: e.message }; - } - `; - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = ` + try { + const proto = {}[{get toString(){ return () => '__proto__' }}]; + if (!proto) return 'blocked_at_proto'; + const ctor = proto[{get toString(){ return () => 'constructor' }}]; + return { type: typeof ctor }; + } catch (e) { + return { blocked: true, error: e.message }; + } + `; + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); it('ATK-ARRCOERCE-47: mixed vectors (template + conditional + sequence)', async () => { const enclave = new Enclave({ timeout: 5000 }); - const code = [ - 'try {', - ' const op = {}[`__proto__`];', - ' if (!op) return "blocked_at_proto";', - ' const ho = op[true ? "constructor" : "x"];', - ' if (!ho) return "blocked_at_constructor";', - ' const glob = ho.getOwnPropertyDescriptors(this);', - ' const trackDesc = glob.__host_memory_track__;', - ' if (!trackDesc || typeof trackDesc.value !== "function") return "no_memory_track";', - ' const F = trackDesc.value[(0, "constructor")];', - ' const fn = F("return process");', - ' const proc = fn();', - ' return {', - ' res: proc.getBuiltinModule("child_process")', - ' .execSync("id").toString().split("\\n"),', - ' };', - '} catch (e) {', - ' return { blocked: true, error: e.message };', - '}', - ].join('\n'); - const result = await enclave.run(code); - assertNoEscape(result); - enclave.dispose(); + try { + const code = [ + 'try {', + ' const op = {}[`__proto__`];', + ' if (!op) return "blocked_at_proto";', + ' const ho = op[true ? "constructor" : "x"];', + ' if (!ho) return "blocked_at_constructor";', + ' const glob = ho.getOwnPropertyDescriptors(this);', + ' const trackDesc = glob.__host_memory_track__;', + ' if (!trackDesc || typeof trackDesc.value !== "function") return "no_memory_track";', + ' const F = trackDesc.value[(0, "constructor")];', + ' const fn = F("return process");', + ' const proc = fn();', + ' return {', + ' res: proc.getBuiltinModule("child_process")', + ' .execSync("id").toString().split("\\n"),', + ' };', + '} catch (e) {', + ' return { blocked: true, error: e.message };', + '}', + ].join('\n'); + const result = await enclave.run(code); + assertNoEscape(result); + } finally { + enclave.dispose(); + } }, 15000); }); });