Summary
Two related issues in python-fire (library + python -m fire entrypoint): (1) OS command injection when an external pager is spawned; (2) arbitrary shell command execution when a user sources a generated Bash completion script, if the CLI name (often derived from a .py filename) contains Bash command substitution or other shell metacharacters.
Repository: https://github.com/google/python-fire
Issue A: PAGER + subprocess.Popen(..., shell=True)
File: fire/console/console_io.py, function More(), approximately lines 81–100.
Behavior: When check_pager is true (default), stdin/stdout are TTYs, and PAGER is set, the value from os.environ['PAGER'] is passed to:
subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True)
Because shell=True, the string is interpreted by /bin/sh. Metacharacters (;, |, `, $(), etc.) allow arbitrary commands to run in the same security context as the Fire-based process.
Trigger: Any code path that calls console_io.More() with the default pager behavior—e.g. fire.core.Display() used for help text, traces, and other paged output—when the process has a malicious or attacker-controlled PAGER and an interactive terminal.
Verification (PoC): With PAGER='touch /tmp/fire_verify_pager', run under a pseudo-TTY (e.g. script -qefc 'python -c "from fire.console import console_io; import sys; console_io.More(\"x\", sys.stdout)"' /dev/null). The side-effect file appears, confirming shell interpretation of PAGER.
Issue B: Bash completion script — unsanitized name / identifier
File: fire/completion.py, function _BashScript(), template bash_completion_template and .format(...) around lines 179–187.
Behavior: The Bash script embeds name and identifier with only a weak transform:
identifier = name.replace('/', '').replace('.', '').replace(',', '')
Characters such as $, (, ), `, ;, newlines, etc. are not removed. The template includes lines such as:
_complete-{identifier}()
complete -F _complete-{identifier} {command}
with command=name. When the generated script is sourced by Bash, command substitution in identifier or name (e.g. $(touch marker)) is evaluated.
Trigger: python -m fire sets name from os.path.basename(path) (fire/__main__.py, import_from_file_path). A file named like $(touch _marker).py therefore yields a completion script that runs touch (or worse) when the victim runs source on the output of -- --completion.
Verification (PoC): Create a minimal module file literally named $(touch _fire_verify_completion_marker).py, run python -m fire '<path>' -- --completion > comp.sh, then bash -c 'source comp.sh'. The marker file is created (Bash may also error on an invalid function name, but substitution still runs).
Note: A subshell payload must not contain / in the basename (invalid filename on Unix); use e.g. $(touch _marker) with a relative target.
CWE mapping
- Issue A: CWE-78 (OS command injection)
- Issue B: CWE-78 / insufficient neutralization of shell metacharacters in generated shell code
Who can exploit
- Issue A: Anyone who can control the environment of a process that runs Fire and reaches
More() on a TTY with PAGER set (e.g. misconfigured services, wrappers, CI jobs, compromised .env, multi-tenant job runners). The user running the CLI can also set PAGER themselves; the higher risk is untrusted or inherited environment for automated or privileged contexts.
- Issue B: An attacker who can place or name a
.py file on disk (or otherwise influence the name passed to fire.Fire(..., name=...)) and convince a victim to generate and source Bash completion from that module (e.g. cloned repo, malicious path, social engineering). Supply-chain / “run this install script” scenarios are plausible.
What the attacker gains
- Issue A: Arbitrary code execution as the Fire process user: read/write secrets the process can access, lateral movement on the host, data destruction (CIA triad: High).
- Issue B: Arbitrary code execution as the user who sources the completion script in Bash—typically the developer’s or operator’s interactive account.
Prerequisites / limits
- Issue A requires an interactive TTY for the default pager path (
IsInteractive(output=True)); non-TTY falls back to writing without PAGER.
- Issue B requires the victim to execute the generated script in Bash (e.g.
source); reading the file alone is not enough.
Suggested severity (informal)
- Issue A: often assessed High locally (e.g. CVSS ~8.x) when env is attacker-influenced; lower if only self-controlled env.
- Issue B: High when sourcing untrusted completion is realistic; depends on social/engineering and workflow.
Summary
Two related issues in python-fire (library +
python -m fireentrypoint): (1) OS command injection when an external pager is spawned; (2) arbitrary shell command execution when a user sources a generated Bash completion script, if the CLIname(often derived from a.pyfilename) contains Bash command substitution or other shell metacharacters.Repository: https://github.com/google/python-fire
Issue A:
PAGER+subprocess.Popen(..., shell=True)File:
fire/console/console_io.py, functionMore(), approximately lines 81–100.Behavior: When
check_pageris true (default), stdin/stdout are TTYs, andPAGERis set, the value fromos.environ['PAGER']is passed to:subprocess.Popen(pager, stdin=subprocess.PIPE, shell=True)Because
shell=True, the string is interpreted by/bin/sh. Metacharacters (;,|,`,$(), etc.) allow arbitrary commands to run in the same security context as the Fire-based process.Trigger: Any code path that calls
console_io.More()with the default pager behavior—e.g.fire.core.Display()used for help text, traces, and other paged output—when the process has a malicious or attacker-controlledPAGERand an interactive terminal.Verification (PoC): With
PAGER='touch /tmp/fire_verify_pager', run under a pseudo-TTY (e.g.script -qefc 'python -c "from fire.console import console_io; import sys; console_io.More(\"x\", sys.stdout)"' /dev/null). The side-effect file appears, confirming shell interpretation ofPAGER.Issue B: Bash completion script — unsanitized
name/identifierFile:
fire/completion.py, function_BashScript(), templatebash_completion_templateand.format(...)around lines 179–187.Behavior: The Bash script embeds
nameandidentifierwith only a weak transform:identifier = name.replace('/', '').replace('.', '').replace(',', '')Characters such as
$,(,),`,;, newlines, etc. are not removed. The template includes lines such as:_complete-{identifier}()complete -F _complete-{identifier} {command}with
command=name. When the generated script is sourced by Bash, command substitution inidentifierorname(e.g.$(touch marker)) is evaluated.Trigger:
python -m firesetsnamefromos.path.basename(path)(fire/__main__.py,import_from_file_path). A file named like$(touch _marker).pytherefore yields a completion script that runstouch(or worse) when the victim runssourceon the output of-- --completion.Verification (PoC): Create a minimal module file literally named
$(touch _fire_verify_completion_marker).py, runpython -m fire '<path>' -- --completion > comp.sh, thenbash -c 'source comp.sh'. The marker file is created (Bash may also error on an invalid function name, but substitution still runs).Note: A subshell payload must not contain
/in the basename (invalid filename on Unix); use e.g.$(touch _marker)with a relative target.CWE mapping
Who can exploit
More()on a TTY withPAGERset (e.g. misconfigured services, wrappers, CI jobs, compromised.env, multi-tenant job runners). The user running the CLI can also setPAGERthemselves; the higher risk is untrusted or inherited environment for automated or privileged contexts..pyfile on disk (or otherwise influence thenamepassed tofire.Fire(..., name=...)) and convince a victim to generate and source Bash completion from that module (e.g. cloned repo, malicious path, social engineering). Supply-chain / “run this install script” scenarios are plausible.What the attacker gains
Prerequisites / limits
IsInteractive(output=True)); non-TTY falls back to writing withoutPAGER.source); reading the file alone is not enough.Suggested severity (informal)