mirror of
https://github.com/anthropics/claude-code-action.git
synced 2026-08-22 03:18:54 +08:00
Harden tag mode tool permissions against prompt injection (#1002)
Two defenses for tag mode where an attacker with repo write access could craft a prompt injection payload in an issue/PR to gain RCE on the Actions runner: 1. git-push wrapper (H1 #3556799) The Bash(git\ push:*) rule permitted arbitrary flags and remotes, including combinations that execute shell commands locally. Replaced with scripts/git-push.sh which allowlists exactly 'origin <ref>' with no flags, validates the ref via check-ref-format. Same pattern as scripts/gh.sh. 2. acceptEdits instead of blanket Write/Edit (Asana 1213310082312048) Edit/MultiEdit/Write in allowedTools granted write access to the whole runner filesystem (~/.bashrc etc). Removed from allowedTools and set --permission-mode acceptEdits, which auto-accepts edits inside cwd ($GITHUB_WORKSPACE) and denies outside. Headless SDK has no prompt handler so 'ask' becomes deny. Also: - Noted that create-prompt/index.ts exports ALLOWED_TOOLS env var that nothing reads. The live path is modes/tag/index.ts. Mirrored the fix in both so the file the H1 report likely points to stays in sync. - Updated prompt text (3 callsites) to reference the wrapper. - Updated tests (4 prompt-content asserts, 7 tool-list asserts).
This commit is contained in:
+13
-15
@@ -23,19 +23,15 @@ import { GITHUB_SERVER_URL } from "../github/api/config";
|
||||
import { extractUserRequest } from "../utils/extract-user-request";
|
||||
export type { CommonFields, PreparedContext } from "./types";
|
||||
|
||||
const GIT_PUSH_WRAPPER = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`;
|
||||
|
||||
/** Filename for the user request file, read by the SDK runner */
|
||||
const USER_REQUEST_FILENAME = "claude-user-request.txt";
|
||||
|
||||
// Tag mode defaults - these tools are needed for tag mode to function
|
||||
const BASE_ALLOWED_TOOLS = [
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"LS",
|
||||
"Read",
|
||||
"Write",
|
||||
];
|
||||
// Tag mode defaults - these tools are needed for tag mode to function.
|
||||
// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode
|
||||
// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside it.
|
||||
const BASE_ALLOWED_TOOLS = ["Glob", "Grep", "LS", "Read"];
|
||||
|
||||
export function buildAllowedToolsString(
|
||||
customAllowedTools?: string[],
|
||||
@@ -59,7 +55,7 @@ export function buildAllowedToolsString(
|
||||
baseTools.push(
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
`Bash(${GIT_PUSH_WRAPPER}:*)`,
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
@@ -434,7 +430,7 @@ function getCommitInstructions(
|
||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||
: ""
|
||||
}
|
||||
- Push to the remote: Bash(git push origin HEAD)`;
|
||||
- Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin HEAD)`;
|
||||
} else {
|
||||
const branchName = eventData.claudeBranch || eventData.baseBranch;
|
||||
return `
|
||||
@@ -448,7 +444,7 @@ function getCommitInstructions(
|
||||
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
|
||||
: ""
|
||||
}
|
||||
- Push to the remote: Bash(git push origin ${branchName})`;
|
||||
- Push to the remote: Bash(${GIT_PUSH_WRAPPER} origin ${branchName})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -823,7 +819,7 @@ ${
|
||||
: `- Use git commands via the Bash tool for version control (remember that you have access to these git commands):
|
||||
- Stage files: Bash(git add <files>)
|
||||
- Commit changes: Bash(git commit -m "<message>")
|
||||
- Push to remote: Bash(git push origin <branch>) (NEVER force push)
|
||||
- Push to remote: Bash(${GIT_PUSH_WRAPPER} origin <branch>)
|
||||
- Delete files: Bash(git rm <files>) followed by commit and push
|
||||
- Check status: Bash(git status)
|
||||
- View diff: Bash(git diff)${eventData.isPR && eventData.baseBranch ? `\n - IMPORTANT: For PR diffs, use: Bash(git diff origin/${eventData.baseBranch}...HEAD)` : ""}`
|
||||
@@ -977,7 +973,9 @@ export async function createPrompt(
|
||||
console.log("========================");
|
||||
}
|
||||
|
||||
// Set allowed tools
|
||||
// NOTE: these env var exports are dead — nothing reads ALLOWED_TOOLS / DISALLOWED_TOOLS.
|
||||
// The live path is modes/tag/index.ts which builds --allowedTools into claudeArgs directly.
|
||||
// Kept only so the H1 report's pointed-to file stays in sync with the live fix.
|
||||
const hasActionsReadPermission = false;
|
||||
|
||||
const allAllowedTools = buildAllowedToolsString(
|
||||
|
||||
+11
-8
@@ -114,16 +114,17 @@ export async function prepareTagMode({
|
||||
tool.startsWith("mcp__github_"),
|
||||
);
|
||||
|
||||
// Build claude_args for tag mode with required tools
|
||||
// Tag mode REQUIRES these tools to function properly
|
||||
const gitPushWrapper = `${process.env.GITHUB_ACTION_PATH}/scripts/git-push.sh`;
|
||||
|
||||
// Build claude_args for tag mode with required tools.
|
||||
// Edit/MultiEdit/Write are intentionally omitted: acceptEdits permission mode (set below)
|
||||
// auto-allows file edits inside $GITHUB_WORKSPACE and denies writes outside (e.g. ~/.bashrc).
|
||||
// Listing them here would grant blanket write access to the whole runner (Asana 1213310082312048).
|
||||
const tagModeTools = [
|
||||
"Edit",
|
||||
"MultiEdit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"LS",
|
||||
"Read",
|
||||
"Write",
|
||||
"mcp__github_comment__update_claude_comment",
|
||||
"mcp__github_ci__get_ci_status",
|
||||
"mcp__github_ci__get_workflow_run_details",
|
||||
@@ -137,7 +138,7 @@ export async function prepareTagMode({
|
||||
tagModeTools.push(
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(git push:*)",
|
||||
`Bash(${gitPushWrapper}:*)`,
|
||||
"Bash(git status:*)",
|
||||
"Bash(git diff:*)",
|
||||
"Bash(git log:*)",
|
||||
@@ -171,8 +172,10 @@ export async function prepareTagMode({
|
||||
const escapedOurConfig = ourMcpConfig.replace(/'/g, "'\\''");
|
||||
claudeArgs = `--mcp-config '${escapedOurConfig}'`;
|
||||
|
||||
// Add required tools for tag mode
|
||||
claudeArgs += ` --allowedTools "${tagModeTools.join(",")}"`;
|
||||
// Add required tools for tag mode.
|
||||
// acceptEdits: file edits auto-allowed inside cwd ($GITHUB_WORKSPACE), denied outside.
|
||||
// Headless SDK has no prompt handler, so anything that falls through to "ask" is denied.
|
||||
claudeArgs += ` --permission-mode acceptEdits --allowedTools "${tagModeTools.join(",")}"`;
|
||||
|
||||
// Append user's claude_args (which may have more --mcp-config flags)
|
||||
if (userClaudeArgs) {
|
||||
|
||||
Reference in New Issue
Block a user