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:
59
src/memdir/memoryScan.test.ts
Normal file
59
src/memdir/memoryScan.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { afterEach, expect, test } from 'bun:test'
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import { scanMemoryFiles } from './memoryScan.ts'
|
||||
|
||||
// Finding #42-3: readdir({ recursive: true }) has no depth limit.
|
||||
// A deeply nested directory in the memory dir causes a full unbounded walk.
|
||||
|
||||
let tempDir: string
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await rm(tempDir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test('scanMemoryFiles finds .md files at shallow depth', async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'memoryScan-'))
|
||||
await writeFile(join(tempDir, 'note.md'), '---\nname: test\ntype: user\n---\nContent')
|
||||
|
||||
const controller = new AbortController()
|
||||
const result = await scanMemoryFiles(tempDir, controller.signal)
|
||||
|
||||
expect(result.length).toBe(1)
|
||||
expect(result[0].filename).toBe('note.md')
|
||||
})
|
||||
|
||||
test('scanMemoryFiles ignores MEMORY.md', async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'memoryScan-'))
|
||||
await writeFile(join(tempDir, 'MEMORY.md'), '# index')
|
||||
await writeFile(join(tempDir, 'user_role.md'), '---\nname: role\ntype: user\n---\nContent')
|
||||
|
||||
const controller = new AbortController()
|
||||
const result = await scanMemoryFiles(tempDir, controller.signal)
|
||||
|
||||
expect(result.length).toBe(1)
|
||||
expect(result[0].filename).toBe('user_role.md')
|
||||
})
|
||||
|
||||
test('scanMemoryFiles does not return .md files nested beyond max depth', async () => {
|
||||
tempDir = await mkdtemp(join(tmpdir(), 'memoryScan-'))
|
||||
|
||||
// Shallow file - should be found
|
||||
await writeFile(join(tempDir, 'shallow.md'), '---\nname: shallow\ntype: user\n---\nContent')
|
||||
|
||||
// Deeply nested file (depth 5) - should be excluded
|
||||
const deepDir = join(tempDir, 'd1', 'd2', 'd3', 'd4', 'd5')
|
||||
await mkdir(deepDir, { recursive: true })
|
||||
await writeFile(join(deepDir, 'deep.md'), '---\nname: deep\ntype: user\n---\nContent')
|
||||
|
||||
const controller = new AbortController()
|
||||
const result = await scanMemoryFiles(tempDir, controller.signal)
|
||||
|
||||
const filenames = result.map(r => r.filename)
|
||||
expect(filenames).toContain('shallow.md')
|
||||
// The deeply nested file must not appear
|
||||
expect(filenames.some(f => f.includes('deep.md'))).toBe(false)
|
||||
})
|
||||
@@ -38,8 +38,15 @@ export async function scanMemoryFiles(
|
||||
): Promise<MemoryHeader[]> {
|
||||
try {
|
||||
const entries = await readdir(memoryDir, { recursive: true })
|
||||
// Limit depth to 3 levels to prevent DoS from deep/symlinked directory trees.
|
||||
// Relative paths from readdir use the OS separator, so count separators.
|
||||
const sep = require('path').sep as string
|
||||
const MAX_DEPTH = 3
|
||||
const mdFiles = entries.filter(
|
||||
f => f.endsWith('.md') && basename(f) !== 'MEMORY.md',
|
||||
f =>
|
||||
f.endsWith('.md') &&
|
||||
basename(f) !== 'MEMORY.md' &&
|
||||
(f.split(sep).length - 1) < MAX_DEPTH,
|
||||
)
|
||||
|
||||
const headerResults = await Promise.allSettled(
|
||||
|
||||
Reference in New Issue
Block a user