Skip to content

fix(apodex): prevent saved Bash allow from bypassing typed confirm via substitution - #42

Open
Samurai007AK wants to merge 1 commit into
ApodexAI:mainfrom
Samurai007AK:fix/39-saved-bash-allow-rule-bypass-via-command
Open

fix(apodex): prevent saved Bash allow from bypassing typed confirm via substitution#42
Samurai007AK wants to merge 1 commit into
ApodexAI:mainfrom
Samurai007AK:fix/39-saved-bash-allow-rule-bypass-via-command

Conversation

@Samurai007AK

Copy link
Copy Markdown
Contributor

Summary

  • Hardens PermissionStore._matches against unquoted $(...)/backtick substitution: nested payloads must independently match a saved Bash(...) prefix, otherwise the command is not authorized.
  • Stops assess_with_rules from downgrading CONFIRM to SAFE when the call carries a danger label, so dep-installs and force-pushes still require typed yes.
  • Adds regression tests for echo $(pip install x), backticks, git push --force, and single-quoted literals.

Validation

  • Repro script from Saved Bash allow-rule bypass via command substitution (docstring safety contract disproven) #39: git push --force stays confirm with danger='git force-push'; echo $(pip install evil-pkg) stays confirm with danger='installs dependencies'; single-quoted stays allowed.
  • uv run pytest apodex/tests/test_features.py -k "saved_allow or single_quoted or permission_store or assess_with_rules": 5 passed.
  • Full test_features.py: 84 passed, 4 failed — identical 4 fail on unmodified main (Windows path expectations), no new failures.
  • uv run ruff check apodex/permissions.py apodex/agent_tools.py apodex/tests/test_features.py: passes.
  • uv run pyright apodex/permissions.py apodex/agent_tools.py: 0 errors.
  • uv run python tools/import_smoke.py --stage 1: 288/289 (only fcntl missing on Windows, pre-existing).

Root cause

  1. _matches compared raw &&/|/; segments by prefix, never unwrapping substitution, so echo $(pip install x) looked like "just an echo".
  2. assess_with_rules returned SAFE with the danger label dropped. Since observers.py skips confirm() for SAFE, the typed gate never fired. The hard denylist did not help because both repros are confirm, not deny.

Closes #39

…a substitution

Root cause: PermissionStore._matches compared raw segments, ignoring unquoted command substitution, and assess_with_rules dropped the danger label on downgrade.

Validation: repro script confirms git push --force and echo pip install payloads stay confirm with danger; 5 permission tests pass; ruff and pyright clean.

Closes ApodexAI#39
@Samurai007AK
Samurai007AK marked this pull request as ready for review September 12, 2026 07:42
@Samurai007AK

Samurai007AK commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Hey @dq-ai-dev just put this one up, would love your eyes on it when you get a sec!

This was a sneaky one: a saved Bash(echo) style allow was quietly green-lighting stuff like echo $(pip install evil-pkg) and even git push --force, which is exactly what the permissions docstring said could never happen. Turns out the prefix check never looked inside $(...)/backticks, and the danger label got dropped on the downgrade so the typed-yes gate never fired.

I tightened both layers nested shell now needs its own allow, and anything with a danger tag stays at confirm plus added regression tests next to the existing permission tests. ruff/pyright are clean and the repros from the issue now stay at confirm as they should.

Happy to tweak the approach if you'd rather handle the nested case differently!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Saved Bash allow-rule bypass via command substitution (docstring safety contract disproven)

1 participant