Skip to content

GHSA-h539-c7r8-3xq4 CVE-2026-75915

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

High crates.ionpm
Severity
0.0 / 10
High · CVSS 4.0
Exploit chance (EPSS)
0.68%
Chance of exploitation in the next 30 days. Higher than 52.35% of scored vulnerabilities.
Known exploited
No
Not in CISA's catalog
Weakness
CWE-200 Exposure of Sensitive Information to an Unauthorized Actor
CWE-526 Cleartext Storage of Sensitive Information in an Environment Variable
Fix
Available
3 packages have a fixed version

Description

Maintainer resolution

The CodeWhale maintainers validated this report. The affected package ranges are recorded in the advisory metadata. Version 0.8.64 contains the fix in commit 26de44a8bd5051f8f944ea60b2c37ae1d2b7d25e. Users should upgrade to 0.8.64 or later. The original reporter analysis is preserved below.

Summary

js_execution exposes parent process environment to model-provided JavaScript

The js_execution tool spawns Node with tokio::process::Command::new without calling the child_env scrubber that exec_shell, the Python REPL, and the MCP launcher all use. Model-provided JavaScript reads process.env and the values flow back to the parent transcript as the tool's stdout, exposing API keys, cloud credentials, and forge tokens to the next model turn.

Details

In crates/tui/src/tools/js_execution.rs (v0.8.37, lines 91-105):

let temp_dir = tempfile::tempdir()
    .map_err(|e| ToolError::execution_failed(format!("tempdir failed: {e}")))?;
let script_path = temp_dir.path().join("js_execution.js");
tokio::fs::write(&script_path, code)
    .await
    .map_err(|e| ToolError::execution_failed(format!("tempfile write failed: {e}")))?;

let mut cmd = tokio::process::Command::new(&node);
cmd.arg(&script_path);
cmd.current_dir(workspace);

let output = tokio::time::timeout(Duration::from_secs(120), cmd.output())
    .await
    .map_err(|_| ToolError::Timeout { seconds: 120 })
    .and_then(|res| res.map_err(|e| ToolError::execution_failed(e.to_string())))?;

The Command is built without cmd.env_clear() and without the project's crate::child_env::apply_to_tokio_command helper. Every variable in the parent process environment is inherited by the spawned node.

For comparison, exec_shell (crates/tui/src/tools/shell.rs:790-792) and the Python REPL (crates/tui/src/repl/runtime.rs:238) both apply the scrubber:

child_env::apply_to_command(&mut cmd, child_env::string_map_env(&exec_env.env));

apply_to_tokio_command calls cmd.env_clear() and then re-installs only the keys that pass is_allowed_parent_env_key (PATH, HOME, USER, LANG/LC_*, TMPDIR, proxy variables, Windows toolchain context, terminal settings). Secret-bearing variables (DEEPSEEK_API_KEY, OPENAI_API_KEY, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, GITHUB_TOKEN, etc.) are not on the allowlist and are dropped before the child starts. The js_execution path bypasses both the env_clear and the allowlist.

Commit history makes the gap explicit. Commit e6d4eae fix(security): scrub child process environments (2026-05-08) introduced child_env.rs and rewrote exec_shell, the Python REPL, the MCP launcher, and main.rs to use it. Commit 2566f3c feat(tools): add js_execution tool (2026-05-12) added this file four days later and never picked up the helper.

The tool is described to the model and surfaced in the approval pane as "Run model-provided JavaScript code in local Node.js execution sandbox" (crates/tui/src/core/engine/turn_loop.rs:1174-1176). No sandbox is applied beyond a 120-second timeout; Node has full filesystem and network access in addition to the inherited environment. The wording understates the trust boundary that the user is being asked to cross.

In YOLO mode (auto_approve=true) the JS body runs without any prompt at all, so a single adversarial prompt-injection from a README, fetched web page, or MCP server output drains the parent environment to the next model turn.

PoC

A standalone Cargo test reproduces the unscrubbed-env behavior. Save as crates/tui/tests/js_execution_env_leak.rs and run with cargo test -p deepseek-tui --test js_execution_env_leak -- --nocapture:

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}");
}

Equivalent reproducer against the binary:

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.

The fix is one line added next to the existing cmd.current_dir(workspace) call:

let mut cmd = tokio::process::Command::new(&node);
cmd.arg(&script_path);
cmd.current_dir(workspace);
crate::child_env::apply_to_tokio_command(&mut cmd, std::iter::empty::<(&str, &str)>());

This calls the existing helper with no overrides, mirroring how repl/runtime.rs spawns the Python REPL. The behavior the description string already promises (sandbox) is then partially honored: secret-bearing parent variables stay in the parent.

Impact

The tool returns parent-environment secrets to the model on a single approval, or with no approval in YOLO mode. Any variable the user has exported becomes part of the next model request and travels to the configured LLM provider's logs. Common variables that the codebase's own provider clients read from process env, and therefore the values most likely to be present, include DEEPSEEK_API_KEY, OPENAI_API_KEY, ANTHROPIC_API_KEY, MISTRAL_API_KEY, AZURE_OPENAI_API_KEY, XAI_API_KEY, GROQ_API_KEY, and TOGETHER_API_KEY. Cloud and source-control credentials commonly exported in developer shells include AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN, GOOGLE_APPLICATION_CREDENTIALS, GITHUB_TOKEN, GH_TOKEN, GITLAB_TOKEN, NPM_TOKEN, CARGO_REGISTRY_TOKEN, PYPI_API_TOKEN, and DATABASE_URL-style secrets. The local-sandbox wording shown at approval time understates the trust boundary, so users approving what they read as a sandboxed snippet do not anticipate that every shell-exported credential is reachable from the snippet. The remediation matches the pattern already adopted across exec_shell, the Python REPL, and the MCP launcher, so the gap is a missed call site rather than a design tradeoff.

Affected versions

crates.io

PackageVulnerableFixed in
codewhale-tui
>= 0.8.41, < 0.8.64
0.8.64
deepseek-tui
>= 0.8.32, <= 0.8.41
No fix yet

npm

PackageVulnerableFixed in
codewhale
>= 0.8.41, < 0.8.64
0.8.64
deepseek-tui
>= 0.8.32, < 0.8.41
0.8.41

How to fix

Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.

codewhale-tui crates.io → 0.8.64

cargo
cargo update -p codewhale-tui --precise 0.8.64

codewhale npm → 0.8.64

npm
npm install codewhale@0.8.64
yarn
yarn add codewhale@0.8.64
pnpm
pnpm add codewhale@0.8.64

deepseek-tui npm → 0.8.41

npm
npm install deepseek-tui@0.8.41
yarn
yarn add deepseek-tui@0.8.41
pnpm
pnpm add deepseek-tui@0.8.41

Severity breakdown

CVSS 4.0 0.0

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N

Attack vector
Network
Attack complexity
Low
Attack requirements
None
Privileges required
None
User interaction
None
Confidentiality (vulnerable system)
High
Integrity (vulnerable system)
None
Availability (vulnerable system)
None
Confidentiality (subsequent systems)
None
Integrity (subsequent systems)
None
Availability (subsequent systems)
None

CVSS 3.1 7.5

CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N

Attack vector
Network
Attack complexity
Low
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
High
Integrity
None
Availability
None

Weakness

CWE-200: Exposure of Sensitive Information to an Unauthorized Actor

The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.

How to prevent it
  • Architecture and Design: Compartmentalize the system to have "safe" areas where trust boundaries can be unambiguously drawn. Do not allow sensitive data to go outside of the trust boundary and always be careful when interfacing with a compartment outside of the safe area. Ensure that appropriate compartmentalization is built into the system design, and the compartmentalization allows for and reinforces privilege separation functionality. Architects and designers should rely on the principle of least privilege to decide the appropriate time to use privileges and the time to drop privileges.

CWE-526: Cleartext Storage of Sensitive Information in an Environment Variable

The product uses an environment variable to store unencrypted sensitive information.

How to prevent it
  • Architecture and Design: Encrypt information stored in the environment variable to protect it from being exposed to an unauthorized user. If encryption is not feasible or is considered too expensive for the business use of the application, then consider using a properly protected configuration file instead of an environment variable. It should be understood that unencrypted information in a config file is also not guaranteed to be protected, but it is still a better choice, because it reduces attack surface related to weaknesses such as CWE-214. In some settings, vaults might be a feasible option for safer data transfer. Users should be notified of the business choice made to not protect the sensitive information through encryption.
  • Implementation: If the environment variable is not necessary for the desired behavior, then remove it entirely, or clear it to an empty value.

References

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy it's related
GHSA-w7wx-5q49-r59w
CodeWhale: image_analyze follows workspace symlinks, leaking external file bytes
High Also affects codewhale
GHSA-7j5w-7r7x-9v27
CodeWhale: Argument Injection in `git_show` Tool Allows Arbitrary File Write Without Approval
Critical Also affects codewhale
GHSA-c6mw-8xh8-gpq6
CodeWhale: Argument Injection in `git_blame` Tool Allows Arbitrary File Read Without Approval
High Also affects codewhale
GHSA-gx45-xrj5-g6c4
CodeWhale: Project config `allow_shell` override enables arbitrary shell command execution via cloned repository
High Also affects codewhale
GHSA-62f5-cp2p-vq95
CodeWhale: Project config `instructions` override enables arbitrary file read into AI system prompt via cloned repository
High Also affects codewhale
GHSA-wrj3-vj8c-784f
CodeWhale: rlm_eval auto-approves arbitrary Python execution, bypassing the user's approval policy (RCE)
High Also affects codewhale
GHSA-g29h-pfmp-qp9r
CodeWhale: exec_shell_interact sends LLM-controlled input to a running shell without an approval prompt (privilege escalation)
High Also affects codewhale
GHSA-6v2g-fpxh-pmmh
CodeWhale: SSRF‌ bypass - TOCTOU on DNS failure for DNS pinning
High Also affects codewhale
Is this in your code?

MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.

Check your repos with MergeHarbor