Unverified 提交 af50a6c4 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Allow $(gh ...) command substitution in permission hook (#2622)

## Summary - Add safe pattern for `$(gh ...)` subcommands in the shell injection checker, allowing common patterns like dynamically constructing API endpoint URLs using `gh repo view` output - The inner content must not contain shell metacharacters to prevent nested injection - Add two new test cases to `good_commands.txt` covering `$(gh repo view ...)` and `REPO=$(gh repo view ...) &&` patterns ## Test plan - [x] All 923 hook permission tests pass (516 good + 407 bad) - [x] Unit tests pass (31 test files) - [x] Lint/format/type checks pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2622" target="_blank"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches shell-injection detection logic in a security gate; a too-permissive regex could allow malicious command substitution, though it is constrained to read-only subcommands and covered by new tests. > > **Overview** > **Permission hook now permits a constrained form of `$(gh ...)` substitution.** `gh-permission-hook.py` adds `SAFE_GH_SUBCOMMAND_PATTERN` and neutralizes matching read-only `$(gh …)` subcommands before running shell-metacharacter checks, enabling dynamic `gh api` paths built from `gh repo view`/similar. > > **Tests updated to validate both sides.** `good_commands.txt` adds allowed examples using `$(gh repo view …)` (including a var-assignment pattern that remains passthrough), and `bad_commands.txt` adds cases ensuring destructive subcommands inside `$(...)` are still blocked. `package-lock.json` also changes peer metadata for multiple dependencies (lockfile-only churn). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 8bcb142513e4dfab4689e0160d581f44f7b24e95. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Enabled safe $(gh ...) command substitution in the permission hook for dynamic GitHub API paths, limited to read-only gh subcommands. Tightened validation to block nested injection and newline/CR bypass. - **New Features** - Treat $(gh ...) as safe when the inner command has no shell metacharacters and uses an allowed read-only subcommand (repo/pr/issue/run/release/gist view, search, status, auth status, config get/list), and neutralize it before checks. - Added two good-command tests for gh api with $(gh repo view ...) and REPO=$(gh repo view ...) && patterns. - **Bug Fixes** - Fixed regex to prevent newline/CR bypass by using [ \t] and excluding \n\r in the inner content. - Added five bad-command tests to ensure destructive subcommands inside $(...) are blocked. <sup>Written for commit 8bcb142513e4dfab4689e0160d581f44f7b24e95. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: 's avatarClaude Opus 4.6 <noreply@anthropic.com> Co-authored-by: 's avatarclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
上级 08b2cfa0
......@@ -150,6 +150,20 @@ SAFE_REDIRECT_PATTERN = re.compile(r'\d*>&\d+|\d*>/dev/null')
# The echo command only outputs text, making this safe for fallback values
SAFE_FALLBACK_PATTERN = re.compile(r'\|\|\s*echo\s+(?:"[^"]*"|\'[^\']*\'|\S+)\s*$')
# Safe gh subcommand pattern - $(gh ...) command substitution where the inner
# command is a safe, read-only gh call (no shell metacharacters inside). This is
# commonly used to dynamically construct API endpoint URLs, e.g.:
# gh api repos/$(gh repo view --json nameWithOwner -q .nameWithOwner)/pulls/123/comments
# The inner content must not contain shell metacharacters (;|&`$<>\n\r) to prevent
# nested injection like $(gh pr view 123; rm -rf /)
# Only known read-only subcommands are allowed to prevent destructive commands
# like $(gh repo delete ...) from being neutralized.
SAFE_GH_SUBCOMMAND_PATTERN = re.compile(
r'\$\(gh[ \t]+(?:repo[ \t]+view|pr[ \t]+view|issue[ \t]+view|run[ \t]+view|release[ \t]+view'
r'|gist[ \t]+view|search[ \t]+\w+|status|auth[ \t]+status|config[ \t]+(?:get|list))'
r'[ \t]+[^)$`;&|<>\n\r]*\)'
)
def extract_gh_command(command: str) -> Optional[str]:
"""
......@@ -260,9 +274,13 @@ def contains_shell_injection(cmd: str) -> bool:
# This allows patterns like: grep -E "bug|error"
cmd_without_safe_doubles = SAFE_DOUBLE_QUOTED_PATTERN.sub('""', cmd_with_neutralized_spans)
# Replace safe $(gh ...) subcommands with a placeholder before checking
# This allows patterns like: gh api repos/$(gh repo view --json nameWithOwner -q .nameWithOwner)/...
cmd_to_check = SAFE_GH_SUBCOMMAND_PATTERN.sub('SAFE_GH_SUB', cmd_without_safe_doubles)
# Replace safe pipe destinations with a placeholder before checking
# This allows patterns like: gh api graphql ... | jq '...'
cmd_to_check = SAFE_PIPE_PATTERN.sub(' SAFE_PIPE ', cmd_without_safe_doubles)
cmd_to_check = SAFE_PIPE_PATTERN.sub(' SAFE_PIPE ', cmd_to_check)
# Replace safe redirect patterns (like 2>&1, 2>/dev/null) before checking
# These are standard shell redirects, not command execution
......
......@@ -667,3 +667,16 @@ gh api /orgs/org/memberships/attacker -f role='admin'
gh api /user/gpg_keys -f armored_public_key='-----BEGIN PGP PUBLIC KEY BLOCK-----'
gh api /user/ssh_signing_keys -f key='ssh-rsa AAAA...'
gh api /user/emails -f emails='["attacker@evil.com"]'
# =============================================================================
# DESTRUCTIVE GH SUBCOMMANDS INSIDE $(...) COMMAND SUBSTITUTION
# =============================================================================
# Destructive gh subcommands should NOT be neutralized by SAFE_GH_SUBCOMMAND_PATTERN
# These use $( which triggers shell injection detection since the inner command
# is not in the allowed read-only subcommand list
gh api repos/$(gh repo delete owner/repo)/pulls/123
gh api repos/$(gh repo create new-repo)/pulls/123
gh api repos/$(gh gist delete abc123)/pulls/123
gh api repos/$(gh run cancel 456)/pulls/123
gh api repos/$(gh issue delete 789)/pulls/123
......@@ -758,3 +758,8 @@ gh pr create --title "Test" --body "Run `echo x | sh` here"
gh pr create --title "Test" --body "Check `env` output"
gh pr create --title "Test" --body "Run `whoami` here"
gh pr view 123 --json <(cat)
gh api repos/$(gh repo view --json nameWithOwner -q .nameWithOwner)/pulls/2524/comments --paginate 2>/dev/null | jq -r '.[].body' 2>/dev/null | head -200
# Note: This command passes as a passthrough (exit 0 / no decision), not as an explicit
# auto-approval, because extract_gh_command() cannot parse VAR=$(...) assignment syntax.
# The test framework treats passthrough the same as allow for good_commands.txt.
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner) && gh api "repos/${REPO}/pulls/2524/comments" --paginate | jq -r '.[].body' 2>/dev/null | head -200
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论