fix: redact escaped quoted credentials in observation records

Carry forward the bounded serialized-quote defect identified by PR 2786. Metadata-only observation consent remains separate; exclude the broader provider lifecycle head.

Co-authored-by: 646826 <646826@gmail.com>
This commit is contained in:
haelyra
2026-10-11 02:05:50 -04:00
co-authored by 646826
parent 7f226d069c
commit 7b4e7828ce
2 changed files with 73 additions and 3 deletions
@@ -273,7 +273,7 @@ import json, sys, os, re
# prevent the catastrophic backtracking that pegged python at 100% CPU (#2278).
_SECRET_RE = re.compile(
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
r"""(["'"'"'\s:=]{1,8})"""
r"""(["'"'"'\s:=\\]{1,8})"""
r"((?:bearer|basic|token|bot)\s+)?"
r"([A-Za-z0-9_\-/.+=]{8,256})"
)
@@ -336,14 +336,15 @@ observation = {
"project_name": os.environ.get("PROJECT_NAME_ENV", "global")
}
# Scrub secrets: match common key=value, key: value, and key"value patterns
# Scrub secrets, including JSON-escaped quotes/backslashes in serialized tool I/O.
# This is defense in depth; arbitrary content still requires a metadata-only capture boundary.
# Includes optional auth scheme (e.g., "Bearer", "Basic") before token
# Linear-time secret matcher. Bounded quantifiers and a fixed set of auth
# schemes (instead of a generic [A-Za-z]+\s+ that overlapped the value class)
# prevent the catastrophic backtracking that pegged python at 100% CPU (#2278).
_SECRET_RE = re.compile(
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
r"""(["'"'"'\s:=]{1,8})"""
r"""(["'"'"'\s:=\\]{1,8})"""
r"((?:bearer|basic|token|bot)\s+)?"
r"([A-Za-z0-9_\-/.+=]{8,256})"
)
@@ -0,0 +1,69 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { spawnSync } = require('node:child_process');
const repoRoot = path.resolve(__dirname, '../..');
const runner = path.join(repoRoot, 'scripts/hooks/run-with-flags.js');
let passed = 0;
let failed = 0;
function test(name, fn) {
try { fn(); console.log(` PASS ${name}`); passed += 1; }
catch (error) { console.log(` FAIL ${name}: ${error.message}`); failed += 1; }
}
function capture(phase, input) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-observe-redaction-'));
try {
const data = path.join(root, 'data');
const result = spawnSync(process.execPath, [runner, `${phase}:observe`,
'scripts/hooks/observe-runner.js', 'standard,strict'], {
input: JSON.stringify({ cwd: root, session_id: 'redaction-fixture', ...input }),
cwd: root, encoding: 'utf8', timeout: 12000,
env: { ...process.env, HOME: root, USERPROFILE: root,
CLV2_HOMUNCULUS_DIR: data, CLAUDE_CODE_ENTRYPOINT: 'cli',
ECC_HOOK_PROFILE: 'standard', ECC_SKIP_OBSERVE: '0',
ECC_DISABLED_HOOKS: '', ECC_OBSERVER_ENABLED: 'false' }
});
assert.equal(result.status, 0, result.stderr);
assert.doesNotMatch(result.stderr, /runner failed|timed out/i);
const records = fs.readFileSync(path.join(data, 'observations.jsonl'), 'utf8');
assert.equal(records.trim().split('\n').length, 1);
assert.equal(JSON.parse(records).event, phase === 'pre' ? 'tool_start' : 'tool_complete');
return records;
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
}
console.log('=== Actual registered observation secret redaction ===');
if (process.platform !== 'win32' && spawnSync('python3', ['--version']).status === 0) {
for (const [label, content] of [
['double-quoted source', 'const apiKey = "CANARY_TOKEN_abcdefghijk";'],
['single-quoted source', "const password = 'CANARY_TOKEN_abcdefghijk';"],
['JSON credential', '{"token":"CANARY_TOKEN_abcdefghijk"}'],
['nested escaped source', 'const auth = \\"CANARY_TOKEN_abcdefghijk\\";']
]) {
test(`redacts ${label} before persisting tool input`, () => {
const records = capture('pre', { tool_name: 'Write',
tool_input: { file_path: '/tmp/example.js', content } });
assert.ok(!records.includes('CANARY_TOKEN_abcdefghijk'), 'seeded credential persisted');
assert.ok(records.includes('[REDACTED]'));
});
test(`redacts ${label} before persisting structured tool output`, () => {
const records = capture('post', { tool_name: 'Read',
tool_input: { file_path: '/tmp/example.js' }, tool_response: { content } });
assert.ok(!records.includes('CANARY_TOKEN_abcdefghijk'), 'seeded credential persisted');
assert.ok(records.includes('[REDACTED]'));
});
}
} else {
console.log('Skipping shell fixtures: native Windows runner requires platform CI.');
}
console.log(`Passed: ${passed}`);
console.log(`Failed: ${failed}`);
process.exitCode = failed ? 1 : 0;