Skip to content
GHSA 39cf Qcfw G8pg

GHSA 39cf Qcfw G8pg

github.com • October 1, 2026

system.run lets the caller supply an env dictionary, filtered by ExecEnvSanitizer before it reaches the spawned process. The sanitizer is a denylist that blocks classic hijack variables ( PATH , PATHEXT , NODE_OPTIONS , LD_PRELOAD / DYLD_* , GIT_SSH_COMMAND , secret-name markers, …). It misses the interpreter/tool code-injection variables — GIT_CONFIG_COUNT / GIT_CONFIG_KEY_n / GIT_CONFIG_VALUE_n (and GIT_CONFIG_GLOBAL / _SYSTEM ), DOTNET_STARTUP_HOOKS , and JAVA_TOOL_OPTIONS / _JAVA_OPTIONS . So a caller who can run an allow-listed tool (e.g. git , which the product explicitly supports allowing via a git * rule) can pass a crafted env that makes that tool execute an attacker command — escalating from "run this bounded tool" to arbitrary code execution on the host (CWE-863 / CWE-184).

The sink. HandleRunAsync calls ExecEnvSanitizer.Sanitize(env) ; anything not blocked is passed to CommandRunner as the child process environment.

The denylist. ExecEnvSanitizer._blockedNames + credential-name markers + a LD_ / DYLD_ prefix block. There is no GIT_CONFIG prefix block, and DOTNET_STARTUP_HOOKS / JAVA_TOOL_OPTIONS are absent. GIT_SSH_COMMAND is blocked, but the env-config route that reaches the same core.sshCommand / core.pager / alias.* / core.fsmonitor settings is not.

The primitives. git reads GIT_CONFIG_COUNT + GIT_CONFIG_KEY_ / GIT_CONFIG_VALUE_ as config; alias. = ! runs a shell command on git , and core.pager / core.sshCommand / core.fsmonitor / core.hooksPath all execute a configured command. DOTNET_STARTUP_HOOKS loads an assembly into every spawned .NET process; JAVA_TOOL_OPTIONS injects -javaagent into every JVM.

PoC (benign — the injected command is a marker echo; verified on macOS git + the compiled node)

Sanitizer gap — ExecEnvSanitizer.Sanitize passes all of these through to the child env (proven): GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0, GIT_CONFIG_VALUE_0, GIT_CONFIG_GLOBAL, DOTNET_STARTUP_HOOKS, JAVA_TOOL_OPTIONS, _JAVA_OPTIONS → 7/7 land in Allowed , none in Blocked .

Sanitizer gap — ExecEnvSanitizer.Sanitize passes all of these through to the child env (proven): GIT_CONFIG_COUNT, GIT_CONFIG_KEY_0, GIT_CONFIG_VALUE_0, GIT_CONFIG_GLOBAL, DOTNET_STARTUP_HOOKS, JAVA_TOOL_OPTIONS, _JAVA_OPTIONS → 7/7 land in Allowed , none in Blocked .

Reachability — with git * allowed, this system.run runs an attacker command: system.run { "command": "git st", "env": { "GIT_CONFIG_COUNT": "1", "GIT_CONFIG_KEY_0": "alias.st", "GIT_CONFIG_VALUE_0": "!echo RK_ENV_FIRED" } } Live git executed the alias and printed RK_ENV_FIRED (no tty or remote needed). core.sshCommand ( git ls-remote ), core.pager (paged commands), and core.fsmonitor / core.hooksPath are equivalent one-call variants.

Reachability — with git * allowed, this system.run runs an attacker command:

Live git executed the alias and printed RK_ENV_FIRED (no tty or remote needed). core.sshCommand ( git ls-remote ), core.pager (paged commands), and core.fsmonitor / core.hooksPath are equivalent one-call variants.

CWE-863 (Incorrect Authorization) / CWE-184 (Incomplete List of Disallowed Inputs). On a node with "Run system tools" enabled and an allow-listed interpreter/tool (git is product-supported; dotnet/java where present), a gateway/agent that can issue system.run escalates a bounded tool invocation into arbitrary code execution on the user's Windows host via a crafted env . Confidentiality, integrity, and availability of the host are at risk. Chains with the exec-approval mutation surface: a caller who can .set git * (a non-dangerous rule) plus this env gap reaches code execution end to end.

Suggested fix (recommended direction; the gap is verified)

Complete the sanitizer's existing block-code-injection intent — it already blocks NODE_OPTIONS , PYTHONSTARTUP , GIT_SSH_COMMAND , BASH_ENV , etc. Add:

a GIT_CONFIG prefix block (covers GIT_CONFIG_COUNT / _KEY_n / _VALUE_n / _GLOBAL / _SYSTEM / _PARAMETERS ), mirroring the LD_ / DYLD_ prefix blocks;

DOTNET_STARTUP_HOOKS (and DOTNET_ADDITIONAL_DEPS );

JAVA_TOOL_OPTIONS , _JAVA_OPTIONS , JDK_JAVA_OPTIONS .

A denylist cannot be exhaustive (the same limitation as the exec-approval command validator), so the sound long-term posture is an allowlist of env variable names a remote system.run may override, with anything else stripped — the local user retains full control. Happy to implement and verify whichever direction you choose, with regression tests that these variables are blocked and benign env overrides still pass.

Extracted Entities

Attack Types (1)

Domains (1)

Platforms (2)

Tools (1)