Skip to content

fix: avoid shell=True in run_script.py build helpers - #2240

Open
leepokai wants to merge 2 commits into
TEN-framework:mainfrom
leepokai:fix/subprocess-shell-injection
Open

fix: avoid shell=True in run_script.py build helpers#2240
leepokai wants to merge 2 commits into
TEN-framework:mainfrom
leepokai:fix/subprocess-shell-injection

Conversation

@leepokai

@leepokai leepokai commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Closes #2107

Problem

run_script.py ran every build/start/test command through subprocess.run(..., shell=True).

For most call sites the commands are hardcoded literals, so this was hygiene rather than an exploitable bug. But in default_extension_cpp the TEN_EXTRA_GN_ARGS environment variable was interpolated directly into the shell string:

extra_gn_args = os_module.environ.get("TEN_EXTRA_GN_ARGS", "")
command = (
    f"tgn gen {os} {arch} {BUILD_TYPE} "
    f"-- ten_enable_standalone_test=true {extra_gn_args}"
)
result = subprocess.run(command, shell=True, check=True, env=env)

Anything in that variable was executed as shell code rather than passed as an argument.

Scope

The issue reported one file, but the same pattern existed in seven. Two of them are the .tent templates that tman uses to scaffold new C++ apps and extensions, so every newly generated project inherited it — fixing only the reported file would have let the pattern keep reproducing.

  • packages/core_apps/default_app_cpp/tools/run_script.py (+ .tent)
  • packages/core_extensions/default_extension_cpp/tools/run_script.py (+ .tent)
  • packages/core_extensions/default_extension_nodejs/tools/run_script.py
  • packages/example_apps/transcriber_demo/ten_packages/extension/vtt_nodejs/tools/run_script.py
  • ai_agents/agents/examples/voice-assistant-nodejs/tenapp/ten_packages/extension/main_nodejs/tools/run_script.py

Change

run_cmd now takes a list[str] and runs without a shell. TEN_EXTRA_GN_ARGS is split with shlex.split, so multi-word and quoted GN args still work while shell metacharacters stay inert.

Two cross-platform details preserved:

  • npm on Windows is spelled npm.cmd. CreateProcess does not apply PATHEXT resolution, so a bare npm with shell=False would fail — this keeps tman_full_win_x64 working.
  • tgn.bat vs tgn branching is unchanged. CreateProcess does execute .bat/.cmd directly, so these still run with shell=False.

Verification

Rather than only inspecting the diff, I stubbed npm/tgn and the built binaries onto PATH, executed each script for real, and asserted the exact argv produced. 22/22 checks pass:

  • All three npm invocations, both tgn gen/tgn build paths, and the start/test subcommands produce the expected argv.
  • TEN_EXTRA_GN_ARGS='vs_version=2022 label="a b"' splits into vs_version=2022 and label=a b.
  • Both .tent templates render ({{package_name}} substituted) and execute correctly.

Injection test with TEN_EXTRA_GN_ARGS='; touch PWNED':

  • Control — the pre-fix code from main created the PWNED file, confirming the injection was real.
  • After — no file is created; the payload arrives as three inert argv tokens ";", "touch", "PWNED".

Formatting checked with black --line-length 79 (the convention 4 of the 5 files already followed). One pre-existing deviation in default_app_cpp sits in an untouched block and was deliberately left alone to avoid unrelated diff noise.

Threat model (scoping this honestly)

To avoid overstating severity:

  • In the file the issue names (main_nodejs/tools/run_script.py) the three commands are hardcoded literals with no external input flowing in, so shell=True there was not exploitable — it is a static-analysis finding, not a live vulnerability.
  • TEN_EXTRA_GN_ARGS is the only genuine injection sink, and grep shows nothing in this repo ever sets it (it appears only in a TODO comment in from_scratch.yml). An attacker who can set your build environment variables generally has easier options already.

So this PR is best read as hardening and stopping the pattern from propagating through the templates, not as a fix for an actively exploitable bug. No runtime behaviour changes: the argv reaching npm/tgn is identical, which the argv suite asserts.

subprocess.run(..., shell=True) spawned a shell for every build/start/test
command. In default_extension_cpp the TEN_EXTRA_GN_ARGS environment variable
was interpolated directly into that shell string, so its contents were
executed as shell code rather than passed as arguments.

Pass argument lists and run without a shell instead. TEN_EXTRA_GN_ARGS is
split with shlex, and npm is spelled npm.cmd on Windows because CreateProcess
does not apply PATHEXT resolution.

The .tent templates are updated as well, so newly generated apps and
extensions no longer inherit the pattern.

Closes TEN-framework#2107
from_scratch.yml patches the generated run_script.py on Windows with
`sed s/ten_enable_standalone_test=true/... vs_version=2022/`. Making the
argument a bare list element turned that into a single glued argv token
`ten_enable_standalone_test=true vs_version=2022` instead of two arguments.

Keep the gn args as one string and expand it with shlex.split, so text
appended to it in place still becomes separate arguments.
@leepokai

Copy link
Copy Markdown
Contributor Author

Pushed a follow-up commit after finding a regression in my own change.

from_scratch.yml (Windows leg, line 867) patches the generated run_script.py:

sed -i 's/ten_enable_standalone_test=true/ten_enable_standalone_test=true vs_version=2022/' .../tools/run_script.py

Because the first commit turned that value into a bare list element, the sed produced one glued argv token "ten_enable_standalone_test=true vs_version=2022" rather than two separate arguments. Under the old shell string the shell re-split it, so this would only have surfaced once the default_extension_cpp template was republished to the registry — a latent break, not an immediate CI failure.

Fix: keep the GN args as a single string and expand it with shlex.split, so text appended in place still becomes separate arguments.

gn_args = "ten_enable_standalone_test=true"
rc = run_cmd([tgn, "gen", os, arch, BUILD_TYPE, "--", *shlex.split(gn_args)])

Verified by simulating both CI seds against the template at HEAD~1 vs. this branch:

check before after
sed 1 — vs_version=2022 is its own argv token pass pass (was fail)
sed 1 — no glued argument pass pass (was fail)
sed 2 (bin/bin\\) — old vs new equivalent pass

sed 2 is Windows-only path syntax and can't resolve on POSIX (execvp does a PATH lookup when the name has no /), so I asserted old/new equivalence there rather than claiming functional coverage I can't produce on macOS.

The original 22-check argv suite still passes, including the injection control.

Note also that this sed is the workaround the TODO at line 865 refers to. This PR does not add TEN_EXTRA_GN_ARGS to the .tent template, so the sed is still required — happy to add template support in a follow-up if you'd like to retire it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant