security: fix 5 findings from issue #42 — env leak, ant gate, depth DoS, URL parse, CA cert

Finding 1 [CRITICAL] — sessionRunner leaks full process.env to child
Extract buildChildEnv() with an explicit allowlist of safe OS/runtime vars.
Child process no longer inherits ANTHROPIC_API_KEY, OPENAI_API_KEY, DB
credentials, or any other secret present in the parent shell environment.
Only CLAUDE_CODE_* bridge vars, PATH, HOME, and standard OS env are passed.

Finding 2 [HIGH] — USER_TYPE=ant activatable by external users
Add isAntEmployee() -> false constant in src/utils/buildConfig.ts.
Replace all three direct process.env.USER_TYPE === 'ant' checks in
setup.ts and onChangeAppState.ts so no external user can activate
Anthropic-internal code paths (commit attribution, system prompt clearing,
dangerously-skip-permissions bypass) by setting USER_TYPE in their shell.

Finding 3 [HIGH] — memoryScan.ts unlimited directory walk
Add MAX_DEPTH=3 guard on readdir({ recursive: true }) results.
Deep or symlink-looped memory directories no longer cause an unbounded
blocking walk before the MAX_MEMORY_FILES cap takes effect.

Finding 5 [HIGH] — buildSdkUrl uses string.includes for protocol detection
Replace apiBaseUrl.includes('localhost') with new URL(apiBaseUrl).hostname
comparison so a remote URL containing 'localhost' in its path no longer
incorrectly gets ws:// (unencrypted) instead of wss://.

Finding 6 [HIGH] — upstream proxy writes unvalidated CA cert to disk
Add isValidPemContent() validation before writeFile in the CA cert download
path. A compromised proxy sending non-PEM data (HTML, JSON, scripts) is now
rejected before it can be appended to the system CA bundle.

Each fix is covered by new unit tests (25 tests across 5 new test files).
All 52 tests pass. Build verified clean on v0.1.7.

Fixes #42

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
gnanam1990
2026-04-02 21:04:10 +05:30
parent 3353101e83
commit 942d09ca9c
12 changed files with 363 additions and 24 deletions

View File

@@ -0,0 +1,42 @@
import { expect, test } from 'bun:test'
import { isValidPemContent } from './upstreamproxy.ts'
// Finding #42-6: The CA cert downloaded from the upstream proxy is written
// to disk without validation. A compromised server could send arbitrary data.
// Fix: validate it contains only valid PEM certificate blocks before writing.
test('isValidPemContent returns true for a valid PEM certificate block', () => {
const pem = [
'-----BEGIN CERTIFICATE-----',
'MIICpDCCAYwCCQDU+pQ4pHgSpDANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls',
'b2NhbGhvc3QwHhcNMjMwMTAxMDAwMDAwWhcNMjQwMTAxMDAwMDAwWjAUMRIwEAYD',
'VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7',
'-----END CERTIFICATE-----',
].join('\n')
expect(isValidPemContent(pem)).toBe(true)
})
test('isValidPemContent returns true for multiple PEM blocks', () => {
const block = '-----BEGIN CERTIFICATE-----\nABCD\n-----END CERTIFICATE-----'
const pem = `${block}\n${block}`
expect(isValidPemContent(pem)).toBe(true)
})
test('isValidPemContent returns false for arbitrary text', () => {
expect(isValidPemContent('Hello world')).toBe(false)
expect(isValidPemContent('<html><body>error</body></html>')).toBe(false)
expect(isValidPemContent('{"error":"unauthorized"}')).toBe(false)
})
test('isValidPemContent returns false for empty string', () => {
expect(isValidPemContent('')).toBe(false)
})
test('isValidPemContent returns false for whitespace only', () => {
expect(isValidPemContent(' \n ')).toBe(false)
})
test('isValidPemContent returns false for malformed PEM (no end marker)', () => {
expect(isValidPemContent('-----BEGIN CERTIFICATE-----\nABCD')).toBe(false)
})

View File

@@ -203,6 +203,18 @@ export function resetUpstreamProxyForTests(): void {
state = { enabled: false }
}
/**
* Validate that a string contains only well-formed PEM certificate blocks.
* Used to guard against a compromised upstream proxy sending arbitrary data
* that would be written into the system CA bundle.
*/
export function isValidPemContent(content: string): boolean {
if (!content || !content.trim()) return false
const pemBlockRegex =
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/g
return pemBlockRegex.test(content)
}
async function readToken(path: string): Promise<string | null> {
try {
const raw = await readFile(path, 'utf8')
@@ -271,6 +283,13 @@ async function downloadCaBundle(
return false
}
const ccrCa = await resp.text()
if (!isValidPemContent(ccrCa)) {
logForDebugging(
`[upstreamproxy] ca-cert response is not valid PEM; proxy disabled`,
{ level: 'warn' },
)
return false
}
const systemCa = await readFile(systemCaPath, 'utf8').catch(() => '')
await mkdir(join(outPath, '..'), { recursive: true })
await writeFile(outPath, systemCa + '\n' + ccrCa, 'utf8')