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:
kashyap murali
2026-03-12 13:35:17 -07:00
committed by GitHub
parent 5d0cc745cd
commit f956510b1a
5 changed files with 84 additions and 42 deletions
+23 -18
View File
@@ -1,6 +1,6 @@
#!/usr/bin/env bun
import { describe, test, expect } from "bun:test";
import { describe, test, expect, beforeAll } from "bun:test";
import {
generatePrompt,
getEventTypeAndContext,
@@ -9,6 +9,10 @@ import {
} from "../src/create-prompt";
import type { PreparedContext } from "../src/create-prompt";
beforeAll(() => {
process.env.GITHUB_ACTION_PATH = "/test/action/path";
});
describe("generatePrompt", () => {
const mockGitHubData = {
contextData: {
@@ -505,7 +509,7 @@ describe("generatePrompt", () => {
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain PR-specific instructions (git commands when not using signing)
expect(prompt).toContain("git push");
expect(prompt).toContain("scripts/git-push.sh origin");
expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
@@ -643,7 +647,7 @@ describe("generatePrompt", () => {
const prompt = await generatePrompt(envVars, mockGitHubData, false, "tag");
// Should contain open PR instructions (git commands when not using signing)
expect(prompt).toContain("git push");
expect(prompt).toContain("scripts/git-push.sh origin");
expect(prompt).toContain(
"Always push to the existing branch when triggered on a PR",
);
@@ -757,7 +761,7 @@ describe("generatePrompt", () => {
expect(prompt).toContain("Use git commands via the Bash tool");
expect(prompt).toContain("git add");
expect(prompt).toContain("git commit");
expect(prompt).toContain("git push");
expect(prompt).toContain("scripts/git-push.sh origin");
// Should use the minimal comment tool
expect(prompt).toContain("mcp__github_comment__update_claude_comment");
@@ -886,17 +890,18 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString();
// The base tools should be in the result
expect(result).toContain("Edit");
// Edit/MultiEdit/Write are NOT in allowedTools — acceptEdits permission mode handles them
expect(result).not.toContain("Edit");
expect(result).not.toContain("Write");
expect(result).toContain("Glob");
expect(result).toContain("Grep");
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
// Default is no commit signing, so should have specific Bash git commands
expect(result).toContain("Bash(git add:*)");
expect(result).toContain("Bash(git commit:*)");
expect(result).toContain("Bash(git push:*)");
expect(result).toContain("scripts/git-push.sh:*)");
expect(result).toContain("mcp__github_comment__update_claude_comment");
// Should not have commit signing tools
@@ -908,12 +913,12 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString([], false, false);
// The base tools should be in the result
expect(result).toContain("Edit");
expect(result).not.toContain("Edit");
expect(result).toContain("Glob");
expect(result).toContain("Grep");
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
expect(result).not.toContain("Write");
// Should have specific Bash git commands for non-signing mode
expect(result).toContain("Bash(git add:*)");
@@ -930,7 +935,7 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString(customTools);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Read");
expect(result).toContain("Glob");
// Custom tools should be appended
@@ -950,7 +955,7 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString([], true);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Read");
expect(result).toContain("Glob");
// GitHub Actions tools should be included
@@ -964,7 +969,7 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString(customTools, true);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Read");
// Custom tools should be included
expect(result).toContain("Tool1");
@@ -980,12 +985,12 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString([], false, true);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).not.toContain("Edit");
expect(result).toContain("Glob");
expect(result).toContain("Grep");
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
expect(result).not.toContain("Write");
// Commit signing tools should be included
expect(result).toContain("mcp__github_file_ops__commit_files");
@@ -1001,17 +1006,17 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString([], false, false);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).not.toContain("Edit");
expect(result).toContain("Glob");
expect(result).toContain("Grep");
expect(result).toContain("LS");
expect(result).toContain("Read");
expect(result).toContain("Write");
expect(result).not.toContain("Write");
// Specific Bash git commands should be included
expect(result).toContain("Bash(git add:*)");
expect(result).toContain("Bash(git commit:*)");
expect(result).toContain("Bash(git push:*)");
expect(result).toContain("scripts/git-push.sh:*)");
expect(result).toContain("Bash(git status:*)");
expect(result).toContain("Bash(git diff:*)");
expect(result).toContain("Bash(git log:*)");
@@ -1030,7 +1035,7 @@ describe("buildAllowedToolsString", () => {
const result = buildAllowedToolsString(customTools, true, false);
// Base tools should be present
expect(result).toContain("Edit");
expect(result).toContain("Read");
expect(result).toContain("Bash(git add:*)");
// Custom tools should be included