Summary
src/env.ts — saveKey() writes user-provided API keys directly to ~/.probus/.env by string concatenation without stripping newline characters. An attacker or malicious input that embeds a literal \n or \r\n in the key value can inject arbitrary additional KEY=VALUE lines into the .env file. On next startup, loadDotenv() parses and loads those injected variables into process.env, potentially overwriting ANTHROPIC_API_KEY, PATH, or other sensitive environment variables.
Panel verdict: P1, VERIFIED, EXISTING_DEFECT. Elevated from P2 after Judge upheld Correctness Hawk's analysis.
Affected Files
| File |
Lines |
Issue |
src/env.ts |
L49–L59 |
saveKey() writes raw value with no newline sanitisation |
src/env.ts |
L21–L36 |
parse() splits on \n — treats injected lines as real entries |
src/server/routes.ts |
L141–L155 |
POST /api/keys only calls .trim(), does not strip embedded newlines |
Root Cause — Code Evidence
saveKey() writes raw value (src/env.ts, lines 49–59):
export function saveKey(key: string, value: string): void {
const dir = envDir();
const file = envFile();
mkdirSync(dir, { recursive: true });
const existing = existsSync(file) ? parse(readFileSync(file, 'utf8')) : {};
existing[key] = value; // ← value may contain '\n'
const body = Object.entries(existing)
.map(([k, v]) => `${k}=${v}`) // ← newlines in v break the format
.join('\n') + '\n';
writeFileSync(file, body);
// ...
}
The route only trims whitespace (src/server/routes.ts, lines 146–150):
const trimmed = (key ?? '').trim(); // trim() removes leading/trailing whitespace
// but does NOT remove embedded \n or \r\n
if (!trimmed) return res.status(400).json({ error: 'Key cannot be empty' });
try {
const envVar = envVarForProvider(provider);
saveKey(envVar, trimmed); // ← trimmed may still contain internal newlines
Injection payload example:
POST /api/keys
{ "provider": "anthropic", "key": "sk-real-key\nPATH=/tmp/evil:/Users/neo/.gemini/antigravity/bin:/Users/neo/Library/Application Support/Antigravity/bin:/Users/neo/.bun/bin:/Users/neo/.antigravity/antigravity/bin:/Users/neo/.opencode/bin:/opt/zerobrew/bin:/Users/neo/.zerobrew/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Users/neo/.local/bin:/Users/neo/.local/bin\nANTHROPIC_API_KEY=stolen" }
Resulting ~/.probus/.env file:
ANTHROPIC_API_KEY=sk-real-key
PATH=/tmp/evil:/Users/neo/.gemini/antigravity/bin:/Users/neo/Library/Application Support/Antigravity/bin:/Users/neo/.bun/bin:/Users/neo/.antigravity/antigravity/bin:/Users/neo/.opencode/bin:/opt/zerobrew/bin:/Users/neo/.zerobrew/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/Users/neo/.local/bin:/Users/neo/.local/bin
ANTHROPIC_API_KEY=stolen
On next loadDotenv() call, PATH and ANTHROPIC_API_KEY are overridden with attacker-controlled values.
Impact
- Credential theft: Overwrite
ANTHROPIC_API_KEY or OPENAI_API_KEY with a stolen key, redirecting all API calls to an attacker-controlled server.
- PATH hijacking: Insert an attacker-controlled directory at the front of
PATH, causing Probus to run malicious binaries (e.g. a fake gh CLI).
- Environment poisoning: Any env var can be injected/overwritten, affecting child processes including Claude Code agents.
Steps to Reproduce
- Start Probus:
npm run dev
- Send a malicious key:
curl -X POST http://127.0.0.1:9090/api/keys \
-H 'Content-Type: application/json' \
-d '{"provider": "anthropic", "key": "sk-test\nEVIL_VAR=injected"}'
- Inspect
~/.probus/.env — observe the injected line.
- Restart Probus and check
process.env.EVIL_VAR — it will be injected.
Remediation
Step 1 — Strip all newline and carriage return characters in saveKey()
export function saveKey(key: string, value: string): void {
// Sanitize: remove all newline, CR, and null characters from both key and value
const safeKey = key.replace(/[\r\n\0]/g, '');
const safeValue = value.replace(/[\r\n\0]/g, '');
if (!safeKey) throw new Error('Key name cannot be empty after sanitization');
const dir = envDir();
const file = envFile();
mkdirSync(dir, { recursive: true });
const existing = existsSync(file) ? parse(readFileSync(file, 'utf8')) : {};
existing[safeKey] = safeValue;
const body = Object.entries(existing).map(([k, v]) => `${k}=${v}`).join('\n') + '\n';
writeFileSync(file, body);
try { chmodSync(file, 0o600); } catch { /* ignore */ }
process.env[safeKey] = safeValue;
}
Step 2 — Quote values in the .env file
// Wrap values in double quotes and escape internal quotes:
const body = Object.entries(existing)
.map(([k, v]) => `${k}="${v.replace(/"/g, '\\"')}"`)
.join('\n') + '\n';
Step 3 — Add API key format validation in the route
// src/server/routes.ts — after trimming
if (!/^[\x20-\x7E]+$/.test(trimmed)) { // printable ASCII only
return res.status(400).json({ error: 'Key contains invalid characters' });
}
References
Summary
src/env.ts—saveKey()writes user-provided API keys directly to~/.probus/.envby string concatenation without stripping newline characters. An attacker or malicious input that embeds a literal\nor\r\nin the key value can inject arbitrary additionalKEY=VALUElines into the.envfile. On next startup,loadDotenv()parses and loads those injected variables intoprocess.env, potentially overwritingANTHROPIC_API_KEY,PATH, or other sensitive environment variables.Panel verdict: P1, VERIFIED, EXISTING_DEFECT. Elevated from P2 after Judge upheld Correctness Hawk's analysis.
Affected Files
src/env.tssaveKey()writes raw value with no newline sanitisationsrc/env.tsparse()splits on\n— treats injected lines as real entriessrc/server/routes.tsPOST /api/keysonly calls.trim(), does not strip embedded newlinesRoot Cause — Code Evidence
saveKey()writes raw value (src/env.ts, lines 49–59):The route only trims whitespace (
src/server/routes.ts, lines 146–150):Injection payload example:
Resulting
~/.probus/.envfile:On next
loadDotenv()call,PATHandANTHROPIC_API_KEYare overridden with attacker-controlled values.Impact
ANTHROPIC_API_KEYorOPENAI_API_KEYwith a stolen key, redirecting all API calls to an attacker-controlled server.PATH, causing Probus to run malicious binaries (e.g. a fakeghCLI).Steps to Reproduce
npm run dev~/.probus/.env— observe the injected line.process.env.EVIL_VAR— it will beinjected.Remediation
Step 1 — Strip all newline and carriage return characters in saveKey()
Step 2 — Quote values in the .env file
Step 3 — Add API key format validation in the route
References