Uh oh!
There was an error while loading. Please reload this page.
fix(security): prevent command injection via shell=True (CWE-78) - #1941
Conversation
afca8ba to
a0efa57CompareCodecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@## master #1941 +/- ##
==========================================
+ Coverage 98.10% 98.23% +0.12%
==========================================
Files 61 61 Lines 2748 2772 +24 ==========================================
+ Hits 2696 2723 +27 + Misses 52 49 -3 ☔ View full report in Codecov by Sentry. |
a0efa57 to
11d6890Compare28f982f to
713e7edCompareThere was a problem hiding this comment.
Pull request overview
This PR hardens command execution throughout Commitizen by separating safe subprocess usage from explicit shell execution, addressing the reported shell=True command-injection path in git operations and related CLI argument handling.
Changes:
- Split command execution into
cmd.run()for argv-based safe execution andcmd.run_shell()for intentional shell use in hooks. - Converted git-related commands and commit/changelog argument plumbing from shell strings to list-based subprocess arguments.
- Updated tests to cover the new command APIs, env passing, hook execution, and list-based CLI/git argument handling.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
tests/utils.py | Updates test helpers to use argv-based git commands and trims branch-name output. |
tests/test_git.py | Revises git tests for list-based command execution and new commit/env behavior. |
tests/test_conf.py | Switches test repo setup to argv-based git init. |
tests/test_cmd.py | Adds direct tests for cmd.run, cmd.run_shell, and env propagation. |
tests/test_bump_hooks.py | Updates hook tests to patch run_shell and adds integration coverage. |
tests/test_bump_create_commit_message.py | Replaces shell-string git commands in bump/changelog tests and keeps hook install via shell. |
tests/conftest.py | Converts fixture setup and GPG/git config helpers to argv-based commands. |
tests/commands/test_commit_command.py | Updates commit-command expectations to list args and adds CLI extra-args parsing coverage. |
tests/commands/test_bump_command.py | Adjusts git command assertions and replaces shell filesystem setup with Path APIs. |
commitizen/hooks.py | Routes user-defined hooks through explicit run_shell(). |
commitizen/git.py | Converts git operations to safe argv execution, passes commit env explicitly, and removes shell-string commit construction. |
commitizen/commands/init.py | Builds pre-commit install as argv instead of a shell string. |
commitizen/commands/commit.py | Changes extra git args from string to list[str] and forwards them directly to git.commit(). |
commitizen/commands/changelog.py | Passes topo-order git-log args as a list. |
commitizen/commands/bump.py | Returns commit args as list[str] for safe downstream execution. |
commitizen/cmd.py | Introduces shared _popen() plus explicit safe run() and shell-based run_shell() APIs. |
commitizen/cli.py | Preserves extra CLI args as a list instead of joining/splitting a string. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
153d8f6 to
e7aa5f8CompareReplace shell=True with list-based subprocess calls for all git.py functions that interpolate user-controlled values (tag names, messages, file paths, git references). This prevents shell injection attacks where malicious values in pyproject.toml could execute arbitrary commands during CI/CD runs of 'cz bump'. Changes: - cmd.run() now accepts str | Sequence[str]; lists use shell=False - git.tag() uses list args (fixes primary attack vector) - git.add() uses list args - git.commit() uses list args + env= for GIT_COMMITTER_DATE - git.tag_exist/is_signed_tag/get_tag_message use list args - git.get_filenames_in_commit() uses list args - git.get_tags() uses list args - git._get_log_as_str_list() uses list args Closes#1918 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
e7aa5f8 to
90b055bCompare
Lee-W
left a comment
There was a problem hiding this comment.
This improvement is nice! One question, but non-blocking
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
The packaged commitizen-branch pre-push hook in .pre-commit-hooks.yaml passes the literal string \..\ as an argv element and relied on shell expansion. After #1941 switched git execution to shell=False (CWE-78 hardening), git received the literal string and aborted with atal: ambiguous argument, breaking every commitizen release after v4.15.0 for users of that hook. Expand env vars explicitly on the --rev-range argument via os.path.expandvars so the hook keeps working without reintroducing shell execution. Unset variables are left literal so git surfaces a clear error instead of being silently rewritten to an empty range. Closes#2003 Co-authored-by: Tim Hsiung <26526132+bearomorphism@users.noreply.github.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Description
Fix command injection vulnerability (CWE-78) where
shell=Trueincmd.run()allowed arbitrary command execution via user-controlled values interpolated into git commands (e.g.,annotated_tag_messageinpyproject.toml).Closes#1918
Changes
Split
cmd.run()into two explicit APIs:cmd.run(Sequence[str])-- executes commands withshell=False(safe, no injection)cmd.run(str)-- deprecated, emitsDeprecationWarning, still usesshell=Truefor backward compatcmd.run_shell(str)-- explicit opt-in forshell=True(only used by user-defined hooks)Converted all git operations to list-based execution (
shell=False):git.tag(),git.add(),git.commit(),git.get_commits(),git.get_tags()git.get_filenames_in_commit(),git.tag_exist(),git.is_signed_tag()git.get_tag_message(),git.get_tag_names(),git.find_git_project_root()git.is_staging_clean(),git.is_git_project(),git.get_core_editor()git._get_log_as_str_list(),git.get_default_branch()Eliminated
argsstring splitting (list->string->list round-trip):git.commit(args=...)andgit.get_commits(args=...)now acceptSequence[str]instead ofstrcli.pypassesextra_cli_argsas a list directly from argparse (no" ".join())bump._get_commit_args()returnslist[str]instead of joined string.split()Other improvements:
git.commit()usesenv={"GIT_COMMITTER_DATE": ...}instead of OS-specific shell tricks (cmd /v /con Windows, env prefix on Unix)_create_commit_cmd_string()helper (no longer needed)get_tags()andget_tag_message()format strings (fixes a Windows bug where literal quotes appeared in output)hooks.pyusescmd.run_shell()-- intentionally shell-based for user-defined hooks with pipes/redirectsChecklist
Was generative AI tooling used to co-author this PR?
Generated-by: GitHub Copilot CLI following the guidelines
Code Changes
uv run poe alllocally to ensure this change passes linter check and testsDocumentation Changes
N/A - Internal implementation change only, no user-facing behavior or CLI changes.
Expected Behavior
User-controlled values (tag names, messages, file paths) are passed as literal arguments to git commands without shell interpretation, preventing command injection attacks in CI/CD pipelines.
Steps to Test This Pull Request
pyproject.tomlwith a maliciousannotated_tag_message:cz bump --yesINJECTEDis NOT printed (command not executed)Verification: All Attack Vectors from #1918 Addressed
cmd.run(f'git tag {option} {tag} -m "{msg or tag}"')cmd.run(["git", "tag", option, tag, "-m", msg or tag])(line 177)cmd.run(f"git tag {tag}")cmd.run(["git", "tag", tag])(line 171)cmd.run(f"git add {' '.join(args)}")cmd.run(["git", "add", *args])(line 181)subprocess.Popen(cmd, shell=True)shell=Falsefor all list-basedcmd.run()callsManual validation result: Running
cz bumpwith a maliciousannotated_tag_message = 'Release" && echo INJECTED && echo "'-- the injected command was NOT executed, and the tag was created with the literal string as its message.Backward Compatibility
cmd.run()accepts bothstr(deprecated, emitsDeprecationWarning, usesshell=True) andSequence[str](safe,shell=False). External plugins callingcmd.run("git ...")will continue to work but receive a deprecation warning guiding them to migrate.Additional Context
Related: CWE-78 (OS Command Injection)
The attack vector requires a poisoned
pyproject.toml(e.g., via malicious PR) that gets executed in CI/CD runningcz bump. Withshell=True, values likeannotated_tag_messagewere interpolated into shell command strings, allowing arbitrary command execution. Withshell=False+ list args, each value is a literalargventry -- shell metacharacters (&&,;,|, backticks) have no special meaning.