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 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:
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
Who can exploit
What the attacker gains
Prerequisites / limits
Suggested severity (informal)