GHSA-h539-c7r8-3xq4: Infoleak

Published Sep 4, 2026
·
Updated

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

jsexecution exposes parent process environment to model-provided JavaScript

The jsexecution tool spawns Node with tokio::process::Command::new without calling the childenv scrubber that execshell, 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/jsexecution.rs (v0.8.37, lines 91-105):

rust let tempdir = tempfile::tempdir() .maperr(|e| ToolError::executionfailed(format!("tempdir failed: {e}")))?; let scriptpath = tempdir.path().join("jsexecution.js"); tokio::fs::write(&scriptpath, code) .await .maperr(|e| ToolError::executionfailed(format!("tempfile write failed: {e}")))?;

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

let output = tokio::time::timeout(Duration::fromsecs(120), cmd.output()) .await .maperr(|| ToolError::Timeout { seconds: 120 }) .andthen(|res| res.maperr(|e| ToolError::executionfailed(e.tostring())))?;

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

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

rust childenv::applytocommand(&mut cmd, childenv::stringmapenv(&execenv.env));

applytotokiocommand calls cmd.envclear() and then re-installs only the keys that pass isallowedparentenvkey (PATH, HOME, USER, LANG/LC, TMPDIR, proxy variables, Windows toolchain context, terminal settings). Secret-bearing variables (DEEPSEEKAPIKEY, OPENAIAPIKEY, AWSACCESSKEYID, AWSSECRETACCESSKEY, GITHUBTOKEN, etc.) are not on the allowlist and are dropped before the child starts. The jsexecution path bypasses both the envclear and the allowlist.

Commit history makes the gap explicit. Commit e6d4eae fix(security): scrub child process environments (2026-05-08) introduced childenv.rs and rewrote execshell, the Python REPL, the MCP launcher, and main.rs to use it. Commit 2566f3c feat(tools): add jsexecution 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/turnloop.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 (autoapprove=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/jsexecutionenvleak.rs and run with cargo test -p deepseek-tui --test jsexecutionenvleak -- --nocapture:

rust use deepseektui::tools::jsexecution::executejsexecutiontool; use serdejson::json; use tempfile::tempdir;

#[tokio::test] async fn jsexecutioninheritsparentsecrets() { if deepseektui::dependencies::resolvenode().isnone() { eprintln!("node not on PATH; skipping"); return; } unsafe { std::env::setvar("AWSSECRETACCESSKEY", "leak-marker-AKIA-EXAMPLE"); std::env::setvar("DEEPSEEKAPIKEY", "leak-marker-sk-EXAMPLE"); } let tmp = tempdir().unwrap(); let result = executejsexecutiontool( &json!({"code": "console.log(process.env.AWSSECRETACCESSKEY + '|' + process.env.DEEPSEEKAPIKEY)"}), tmp.path(), ).await.expect("execute"); let payload: serdejson::Value = serdejson::fromstr(&result.content).unwrap(); let stdout = payload["stdout"].asstr().unwrapor(""); 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:

bash export AWSSECRETACCESSKEY="leak-marker-AKIA-EXAMPLE" export DEEPSEEKAPIKEY="leak-marker-sk-EXAMPLE" deepseek Ask the model to run: jsexecution({"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.currentdir(workspace) call:

rust let mut cmd = tokio::process::Command::new(&node); cmd.arg(&scriptpath); cmd.currentdir(workspace); crate::childenv::applytotokiocommand(&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 DEEPSEEKAPIKEY, OPENAIAPIKEY, ANTHROPICAPIKEY, MISTRALAPIKEY, AZUREOPENAIAPIKEY, XAIAPIKEY, GROQAPIKEY, and TOGETHERAPIKEY. Cloud and source-control credentials commonly exported in developer shells include AWSACCESSKEYID, AWSSECRETACCESSKEY, AWSSESSIONTOKEN, GOOGLEAPPLICATIONCREDENTIALS, GITHUBTOKEN, GHTOKEN, GITLABTOKEN, NPMTOKEN, CARGOREGISTRYTOKEN, PYPIAPITOKEN, and DATABASEURL-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 execshell, the Python REPL, and the MCP launcher, so the gap is a missed call site rather than a design tradeoff.

Affected Software

4 affected componentsFixes available
npm/codewhale>=0.8.41<0.8.64
0.8.64
rust/codewhale-tui>=0.8.41<0.8.64
0.8.64
npm/deepseek-tui>=0.8.32<0.8.41
0.8.41
rust/deepseek-tui>=0.8.32<=0.8.41

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/codewhale to a version that resolves this vulnerability.

    Fixed in 0.8.64
  2. Upgrade

    Upgrade rust/codewhale-tui to a version that resolves this vulnerability.

    Fixed in 0.8.64
  3. Upgrade

    Upgrade npm/deepseek-tui to a version that resolves this vulnerability.

    Fixed in 0.8.41
  4. Upgrade

    Upgrade to a fixed release to a version that resolves this vulnerability.

    Fixed in 0.8.64Patch 26de44a8bd5051f8f944ea60b2c37ae1d2b7d25e

Event History

Sep 4, 2026
Advisory Published
via GitHub·06:03 PM
Data Sourced
via GitHub·06:03 PM
DescriptionSeverityWeaknessAffected Software

Frequently Asked Questions

1

What can an attacker obtain through this issue?

Model-provided JavaScript can read the parent process environment and return its values through tool stdout into the parent transcript. Exposed values may include API keys, cloud credentials, and forge tokens.

2

Does exploitation require authentication, user interaction, or local access?

No. The supplied severity vector specifies network attack vector, low attack complexity, no privileges required, and no user interaction.

3

Which component is responsible for the exposure?

The js_execution tool launches Node without using the child environment scrubber used by exec_shell, the Python REPL, and the MCP launcher. As a result, its child process inherits the parent environment.

4

What version should be deployed to remediate the issue?

Upgrade to version 0.8.64 or later. The fix is identified as commit 26de44a8bd5051f8f944ea60b2c37ae1d2b7d25e.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203