mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
test: add 3 edge-case tests for readFile binary, output() NaN/Infinity, loadAliases __proto__ safety
Round 125: Tests for readFile returning garbled strings (not null) on binary files, output() handling undefined/NaN/Infinity as non-objects logged directly (and JSON.stringify converting NaN/Infinity to null in objects), and loadAliases with __proto__ key in JSON proving no prototype pollution occurs. Total: 935 tests, all passing.
This commit is contained in:
@@ -1752,6 +1752,76 @@ function runTests() {
|
||||
'limit > total should return all aliases');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 125: loadAliases with __proto__ key in JSON — no prototype pollution ──
|
||||
console.log('\nRound 125: loadAliases (__proto__ key in JSON — safe, no prototype pollution):');
|
||||
if (test('loadAliases with __proto__ alias key does not pollute Object prototype', () => {
|
||||
// JSON.parse('{"__proto__":...}') creates a normal property named "__proto__",
|
||||
// it does NOT modify Object.prototype. This is safe but worth documenting.
|
||||
// The alias would be accessible via data.aliases['__proto__'] and iterable
|
||||
// via Object.entries, but it won't affect other objects.
|
||||
resetAliases();
|
||||
|
||||
// Write raw JSON string with __proto__ as an alias name.
|
||||
// IMPORTANT: Cannot use JSON.stringify(obj) because {'__proto__':...} in JS
|
||||
// sets the prototype rather than creating an own property, so stringify drops it.
|
||||
// Must write the JSON string directly to simulate a maliciously crafted file.
|
||||
const aliasesPath = aliases.getAliasesPath();
|
||||
const now = new Date().toISOString();
|
||||
const rawJson = `{
|
||||
"version": "1.0.0",
|
||||
"aliases": {
|
||||
"__proto__": {
|
||||
"sessionPath": "/evil/path",
|
||||
"createdAt": "${now}",
|
||||
"title": "Prototype Pollution Attempt"
|
||||
},
|
||||
"normal": {
|
||||
"sessionPath": "/normal/path",
|
||||
"createdAt": "${now}",
|
||||
"title": "Normal Alias"
|
||||
}
|
||||
},
|
||||
"metadata": { "totalCount": 2, "lastUpdated": "${now}" }
|
||||
}`;
|
||||
fs.writeFileSync(aliasesPath, rawJson);
|
||||
|
||||
// Load aliases — should NOT pollute prototype
|
||||
const data = aliases.loadAliases();
|
||||
|
||||
// Verify __proto__ did NOT pollute Object.prototype
|
||||
const freshObj = {};
|
||||
assert.strictEqual(freshObj.sessionPath, undefined,
|
||||
'Object.prototype should NOT have sessionPath (no pollution)');
|
||||
assert.strictEqual(freshObj.title, undefined,
|
||||
'Object.prototype should NOT have title (no pollution)');
|
||||
|
||||
// The __proto__ key IS accessible as a normal property
|
||||
assert.ok(data.aliases['__proto__'],
|
||||
'__proto__ key exists as normal property in parsed aliases');
|
||||
assert.strictEqual(data.aliases['__proto__'].sessionPath, '/evil/path',
|
||||
'__proto__ alias data is accessible normally');
|
||||
|
||||
// Normal alias also works
|
||||
assert.ok(data.aliases['normal'],
|
||||
'Normal alias coexists with __proto__ key');
|
||||
|
||||
// resolveAlias with '__proto__' — rejected by regex (underscores ok but __ prefix works)
|
||||
// Actually ^[a-zA-Z0-9_-]+$ would ACCEPT '__proto__' since _ is allowed
|
||||
const resolved = aliases.resolveAlias('__proto__');
|
||||
// If the regex accepts it, it should find the alias
|
||||
if (resolved) {
|
||||
assert.strictEqual(resolved.sessionPath, '/evil/path',
|
||||
'resolveAlias can access __proto__ alias (regex allows underscores)');
|
||||
}
|
||||
|
||||
// Object.keys should enumerate __proto__ from JSON.parse
|
||||
const keys = Object.keys(data.aliases);
|
||||
assert.ok(keys.includes('__proto__'),
|
||||
'Object.keys includes __proto__ from JSON.parse (normal property)');
|
||||
assert.ok(keys.includes('normal'),
|
||||
'Object.keys includes normal alias');
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Summary
|
||||
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
|
||||
process.exit(failed > 0 ? 1 : 0);
|
||||
|
||||
@@ -2232,6 +2232,82 @@ function runTests() {
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 125: readFile with binary content — returns garbled UTF-8, not null ──
|
||||
console.log('\nRound 125: readFile (binary/non-UTF8 content — garbled, not null):');
|
||||
if (test('readFile with binary content returns garbled string (not null) because UTF-8 decode does not throw', () => {
|
||||
// utils.js line 285: fs.readFileSync(filePath, 'utf8') — binary data gets UTF-8 decoded.
|
||||
// Invalid byte sequences become U+FFFD replacement characters. The function does
|
||||
// NOT return null for binary files (only returns null on ENOENT/permission errors).
|
||||
// This means grepFile/countInFile would operate on corrupted content silently.
|
||||
const tmpDir = fs.mkdtempSync(path.join(utils.getTempDir(), 'r125-binary-'));
|
||||
const testFile = path.join(tmpDir, 'binary.dat');
|
||||
try {
|
||||
// Write raw binary data (invalid UTF-8 sequences)
|
||||
const binaryData = Buffer.from([0x00, 0x80, 0xFF, 0xFE, 0x48, 0x65, 0x6C, 0x6C, 0x6F]);
|
||||
fs.writeFileSync(testFile, binaryData);
|
||||
|
||||
const content = utils.readFile(testFile);
|
||||
assert.ok(content !== null,
|
||||
'readFile should NOT return null for binary files');
|
||||
assert.ok(typeof content === 'string',
|
||||
'readFile always returns a string (or null for missing files)');
|
||||
// The string contains "Hello" (bytes 0x48-0x6F) somewhere in the garbled output
|
||||
assert.ok(content.includes('Hello'),
|
||||
'ASCII subset of binary data should survive UTF-8 decode');
|
||||
// Content length may differ from byte length due to multi-byte replacement chars
|
||||
assert.ok(content.length > 0, 'Non-empty content from binary file');
|
||||
|
||||
// grepFile on binary file — still works but on garbled content
|
||||
const matches = utils.grepFile(testFile, 'Hello');
|
||||
assert.strictEqual(matches.length, 1,
|
||||
'grepFile finds "Hello" even in binary file (ASCII bytes survive)');
|
||||
|
||||
// Non-existent file — returns null (contrast with binary)
|
||||
const missing = utils.readFile(path.join(tmpDir, 'no-such-file.txt'));
|
||||
assert.strictEqual(missing, null,
|
||||
'Missing file returns null (not garbled content)');
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// ── Round 125: output() with undefined, NaN, Infinity — non-object primitives logged directly ──
|
||||
console.log('\nRound 125: output() (undefined/NaN/Infinity — typeof checks and JSON.stringify):');
|
||||
if (test('output() handles undefined, NaN, Infinity as non-objects — logs directly', () => {
|
||||
// utils.js line 273: `if (typeof data === 'object')` — undefined/NaN/Infinity are NOT objects.
|
||||
// typeof undefined → "undefined", typeof NaN → "number", typeof Infinity → "number"
|
||||
// All three bypass JSON.stringify and go to console.log(data) directly.
|
||||
const origLog = console.log;
|
||||
const logged = [];
|
||||
console.log = (...args) => logged.push(args);
|
||||
try {
|
||||
// undefined — typeof "undefined", logged directly
|
||||
utils.output(undefined);
|
||||
assert.strictEqual(logged[0][0], undefined,
|
||||
'output(undefined) logs undefined (not "undefined" string)');
|
||||
|
||||
// NaN — typeof "number", logged directly
|
||||
utils.output(NaN);
|
||||
assert.ok(Number.isNaN(logged[1][0]),
|
||||
'output(NaN) logs NaN directly (typeof "number", not "object")');
|
||||
|
||||
// Infinity — typeof "number", logged directly
|
||||
utils.output(Infinity);
|
||||
assert.strictEqual(logged[2][0], Infinity,
|
||||
'output(Infinity) logs Infinity directly');
|
||||
|
||||
// Object containing NaN — JSON.stringify converts NaN to null
|
||||
utils.output({ value: NaN, count: Infinity });
|
||||
const parsed = JSON.parse(logged[3][0]);
|
||||
assert.strictEqual(parsed.value, null,
|
||||
'JSON.stringify converts NaN to null inside objects');
|
||||
assert.strictEqual(parsed.count, null,
|
||||
'JSON.stringify converts Infinity to null inside objects');
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
}
|
||||
})) passed++; else failed++;
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Test Results ===');
|
||||
console.log(`Passed: ${passed}`);
|
||||
|
||||
Reference in New Issue
Block a user