Skip to content

vault: avoid shell=True in subprocess calls#2280

Merged
berendt merged 3 commits into
mainfrom
fix/vault-shell-injection
May 19, 2026
Merged

vault: avoid shell=True in subprocess calls#2280
berendt merged 3 commits into
mainfrom
fix/vault-shell-injection

Conversation

@ideaship
Copy link
Copy Markdown
Contributor

@ideaship ideaship commented May 18, 2026

Summary

  • Replace shell=True string interpolation with list form in View.take_action
    and Decrypt.take_action — a user-supplied path containing shell metacharacters
    (e.g. ;) could cause arbitrary command execution
  • Propagate the ansible-vault exit code from Decrypt.take_action, which
    previously discarded it while View.take_action already returned it
  • Guard both View and Decrypt against a missing path argument: nargs="?"
    makes the argument optional, but the code called os.path.isabs(None) and
    raised an unhelpful TypeError when it was omitted

Test plan

  • pipenv run pytest tests/unit/commands/test_vault.py — all 11 tests pass
  • pipenv run pytest — full suite clean

🤖 Generated with Claude Code

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In View.take_action, the subprocess.call invocation now passes path directly instead of str(path), which will cause a mismatch with the updated tests (and may pass a Path object to subprocess); convert path to str(path) before including it in the argument list for consistency and correctness.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `View.take_action`, the `subprocess.call` invocation now passes `path` directly instead of `str(path)`, which will cause a mismatch with the updated tests (and may pass a `Path` object to `subprocess`); convert `path` to `str(path)` before including it in the argument list for consistency and correctness.

## Individual Comments

### Comment 1
<location path="osism/commands/vault.py" line_range="107" />
<code_context>
         if not os.path.isabs(path):
             path = os.path.join("/opt/configuration", path)
-        subprocess.call(f"/usr/local/bin/ansible-vault decrypt {path}", shell=True)
+        subprocess.call(["/usr/local/bin/ansible-vault", "decrypt", path])


</code_context>
<issue_to_address>
**issue (bug_risk):** Align decrypt behavior with view by returning or handling the subprocess exit code.

Right now `decrypt` ignores the return value from `subprocess.call`, so the CLI can report success even if `ansible-vault decrypt` fails. Please propagate the exit code (e.g., return it from `take_action`) or raise on failure (e.g., `subprocess.run([...], check=True)`) so errors are visible to callers.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread osism/commands/vault.py Outdated
ideaship added 3 commits May 19, 2026 12:50
Both View.take_action and Decrypt.take_action passed user-supplied
file paths into a shell command string with shell=True. A path
containing shell metacharacters (e.g. a semicolon) could cause
arbitrary command execution.

Replace the string form with a list, which bypasses the shell
entirely and passes the path as a literal argument:

  subprocess.call(["/usr/local/bin/ansible-vault", "view", path])

Apply the same fix to Decrypt.take_action, which had the identical
pattern but was not part of the original PR that introduced View.

Update existing tests to assert the list form and add a new test
for Decrypt, which previously had no subprocess coverage.

SecurityImpact: eliminates shell injection via crafted filenames
AI-assisted: Claude Code
Signed-off-by: Roger Luethi <luethi@osism.tech>
Decrypt.take_action() discarded the subprocess return code, while
View.take_action() already returned it (added in d9384be). Callers
that check $? or inspect the cliff return value would silently see
success even when ansible-vault decrypt failed.

Add 'return' so the exit code is propagated to the caller, matching
the behaviour of View. Add a test that verifies a non-zero exit code
from subprocess.call() is returned by take_action().

AI-assisted: Claude Code
Signed-off-by: Roger Luethi <luethi@osism.tech>
Both View and Decrypt define 'path' with nargs="?", making it
optional. When omitted, parsed_args.path is None, causing
os.path.isabs(None) to raise a TypeError before any useful error
message reaches the user.

Add an explicit None check at the top of each take_action() that logs
a clear error and returns 1, matching the error-handling style already
used for FileNotFoundError and PermissionError in View. Add tests for
both commands that confirm the exit code and error log when path is
omitted.

AI-assisted: Claude Code
Signed-off-by: Roger Luethi <luethi@osism.tech>
@ideaship ideaship force-pushed the fix/vault-shell-injection branch from 8c93171 to c71178a Compare May 19, 2026 10:51
@ideaship ideaship requested a review from berendt May 19, 2026 12:12
@ideaship ideaship moved this from Ready to In review in Human Board May 19, 2026
@berendt berendt merged commit 1733242 into main May 19, 2026
3 checks passed
@berendt berendt deleted the fix/vault-shell-injection branch May 19, 2026 12:24
@github-project-automation github-project-automation Bot moved this from In review to Done in Human Board May 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants