Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 43 additions & 6 deletions .github/workflows/_claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,14 @@ on:
description: "Personal Access Token with repo scope. Required for: (1) resolving review threads via GraphQL API, and (2) auto-fix pushes that trigger subsequent workflow runs. Falls back to GITHUB_TOKEN if not provided, but auto-fix pushes with GITHUB_TOKEN will NOT trigger new workflow runs."
required: false

# Disable Bun's automatic bunfig.toml loading from CWD. This workflow runs
# `bun run` against post-review scripts; if a caller checks out PR head
# content first, `bun run` would auto-load bunfig.toml from the checkout
# root and `bunfig.toml.preload` would execute arbitrary code with the
# caller's secrets. Propagates to all jobs and steps in this workflow.
env:
BUN_CONFIG_FILE: /dev/null

jobs:
claude-review:
runs-on: ubuntu-latest
Expand All @@ -275,22 +283,51 @@ jobs:
# via attacker-controlled config files (bunfig.toml, .npmrc, etc.) in fork PRs.
# If a legitimate host is missing, the workflow will fail loudly — update the
# allowlist rather than switching back to audit mode.
# Per .github/workflows/CLAUDE.md, bullfrog must be the FIRST step in
# every job on a non-macOS runner ("no exceptions"), so it runs before
# the toolkit_ref validation below. Ordering is safe: bullfrog does not
# consume toolkit_ref, and the validation runs before any step that
# downloads action.yml or post-*.ts content using it.
- name: Security Scan
uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3 # v0.8.4
with:
egress-policy: block
# Per-workflow tightened allowlist. Audit-mode sampling missed
# several destinations because bullfrog v0.8.4 queues DNS *replies*
# (agent/queue_audit.nft) and cached resolutions emit no further
# events. Domains added below as block-mode CI failures revealed:
# - raw.githubusercontent.com: build-plugin-config fetches
# marketplace.json from /Uniswap/ai-toolkit/next/.claude-plugin/.
# - claude.ai: Claude SDK install path (claude.ai/install.sh)
# AND runtime telemetry/session lookup on API-key auth too.
allowed-domains: |
github.com
*.github.com
*.githubusercontent.com
release-assets.githubusercontent.com
raw.githubusercontent.com
api.anthropic.com
claude.ai
*.claude.ai
bun.sh
downloads.claude.ai
registry.npmjs.org
*.npmjs.org
enable-sudo: false

# Validate toolkit_ref BEFORE any downstream step uses it to download
# action.yml / post-*.ts script content from the ai-toolkit repo.
# Without this, an attacker-controlled ref could substitute malicious
# script content that runs inside this job with access to its secrets.
- name: Validate toolkit_ref
env:
TOOLKIT_REF: ${{ inputs.toolkit_ref }}
run: |
case "$TOOLKIT_REF" in
main|next) ;;
*)
if ! printf '%s' "$TOOLKIT_REF" | grep -qE '^[0-9a-fA-F]{40}$'; then
echo "::error::Invalid toolkit_ref: '$TOOLKIT_REF'. Allowed: main, next, or a full 40-char SHA."
exit 1
fi
;;
esac
echo "✅ toolkit_ref validated: $TOOLKIT_REF"

# Checkout required before using local composite actions
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
93 changes: 73 additions & 20 deletions .github/workflows/_claude-docs-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,14 @@ on:
description: "Number of commits pushed when auto_commit is enabled"
value: ${{ jobs.docs-check.outputs.commits_pushed }}

# Disable Bun's automatic bunfig.toml loading from CWD. When a workflow
# checks out PR head content (this workflow checks out PR refs to read
# diffs and post comments), `bun run` would otherwise auto-load

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified an issue in your code:
Reusable workflow (on: workflow_call) uses bun run. Bun auto-loads bunfig.toml from CWD, and its preload array executes arbitrary code before the intended script. If the workflow checks out fork PR code, this enables RCE with access to the caller's secrets. Fix: set BUN_CONFIG_FILE=/dev/null in the job env.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
# diffs and post comments), `bun run` would otherwise auto-load
BUN_CONFIG_FILE=/dev/null bun run "$POST_SCRIPT" $SCRIPT_ARGS
View step-by-step instructions
  1. Update the line that runs the Bun script to set the environment variable BUN_CONFIG_FILE to /dev/null to prevent Bun from auto-loading an attacker-controlled configuration file.
  2. Change the execution line to: BUN_CONFIG_FILE=/dev/null bun run "$POST_SCRIPT" $SCRIPT_ARGS.

This protects the workflow from remote code execution via malicious bunfig.toml files in forked pull requests.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by bun-run-in-reusable-workflow.

You can view more details about this finding in the Semgrep AppSec Platform.

# bunfig.toml from the checkout root, and `bunfig.toml.preload` executes
# arbitrary code before the intended script — RCE with caller secrets.
env:
BUN_CONFIG_FILE: /dev/null

jobs:
docs-check:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -197,22 +205,52 @@ jobs:
steps:
# Security scanning — block mode prevents secret exfiltration if RCE is achieved.
# Allowlist validated against Datadog (service:github-actions, 7d window).
# Per .github/workflows/CLAUDE.md, bullfrog must be the FIRST step in
# every job on a non-macOS runner ("no exceptions"), so it runs before
# the toolkit_ref validation below. Ordering is safe: bullfrog does not
# consume toolkit_ref, and the validation runs before any step that
# downloads action.yml or post-*.ts content using it.
- name: Security Scan
uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3 # v0.8.4
with:
egress-policy: block
# Per-workflow tightened allowlist. Audit-mode sampling missed
# several destinations (raw.githubusercontent.com, claude.ai)
# because bullfrog v0.8.4 queues DNS *replies*
# (agent/queue_audit.nft) and cached resolutions emit no further
# events. Add domains here as block-mode CI failures reveal them.
# raw.githubusercontent.com: build-plugin-config fetches
# marketplace.json (Uniswap/ai-toolkit/next/.claude-plugin/).
# claude.ai: Claude SDK install path (https://claude.ai/install.sh)
# AND runtime telemetry/session lookup even on API-key auth.
allowed-domains: |
github.com
*.github.com
*.githubusercontent.com
release-assets.githubusercontent.com
raw.githubusercontent.com
api.anthropic.com
claude.ai
*.claude.ai
bun.sh
downloads.claude.ai
registry.npmjs.org
*.npmjs.org
enable-sudo: false

# Validate toolkit_ref BEFORE any downstream step uses it to download
# action.yml / post-*.ts script content from the ai-toolkit repo.
# Without this, an attacker-controlled ref could substitute malicious
# script content that runs inside this job with access to its secrets.
- name: Validate toolkit_ref
env:
TOOLKIT_REF: ${{ inputs.toolkit_ref }}
run: |
case "$TOOLKIT_REF" in
main|next) ;;
*)
if ! printf '%s' "$TOOLKIT_REF" | grep -qE '^[0-9a-fA-F]{40}$'; then
echo "::error::Invalid toolkit_ref: '$TOOLKIT_REF'. Allowed: main, next, or a full 40-char SHA."
exit 1
fi
;;
esac
echo "✅ toolkit_ref validated: $TOOLKIT_REF"

# Initial checkout
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -507,18 +545,30 @@ jobs:

PROMPT_EOF

# Substitute variables
sed -i "s|\${REPO_OWNER}|$REPO_OWNER|g" /tmp/docs-check-prompt.txt
sed -i "s|\${REPO_NAME}|$REPO_NAME|g" /tmp/docs-check-prompt.txt
sed -i "s|\${PR_NUMBER}|$PR_NUMBER|g" /tmp/docs-check-prompt.txt
sed -i "s|\${BASE_REF}|$BASE_REF|g" /tmp/docs-check-prompt.txt
sed -i "s|\${LINES_CHANGED}|$LINES_CHANGED|g" /tmp/docs-check-prompt.txt
sed -i "s|\${FAIL_ON_MISSING_DOCS}|$FAIL_ON_MISSING_DOCS|g" /tmp/docs-check-prompt.txt
sed -i "s|\${FAIL_ON_MISSING_VERSION}|$FAIL_ON_MISSING_VERSION|g" /tmp/docs-check-prompt.txt

# Insert changed files (escape for sed)
CHANGED_FILES_ESCAPED=$(printf '%s\n' "$CHANGED_FILES" | sed 's/[&/\]/\\&/g; s/$/\\n/' | tr -d '\n')
sed -i "s|\${CHANGED_FILES}|$CHANGED_FILES_ESCAPED|g" /tmp/docs-check-prompt.txt
# Substitute placeholders using python's str.replace.
#
# Why not sed: the previous code used `sed -i "s|\${VAR}|$VALUE|g"`
# with `|` as the sed delimiter. The escape applied to $CHANGED_FILES
# was `sed 's/[&/\]/\\&/g'`, which escapes `&`, `/`, `\` but NOT `|`
# (the actual delimiter). A filename in $CHANGED_FILES containing `|`
# — legal on Linux, not quoted by git's default `core.quotepath`
# since `|` is in the printable ASCII range — would break the
# substitution into garbage flags and cause the docs-check workflow
# to fail loudly or, worse, write a partial value.
#
# Python's str.replace is plain-text and immune to this class.
export REPO_OWNER REPO_NAME CHANGED_FILES
python3 - <<'PYEOF'
import os
from pathlib import Path
p = Path('/tmp/docs-check-prompt.txt')
content = p.read_text()
for key in ('REPO_OWNER', 'REPO_NAME', 'PR_NUMBER', 'BASE_REF',
'LINES_CHANGED', 'FAIL_ON_MISSING_DOCS',
'FAIL_ON_MISSING_VERSION', 'CHANGED_FILES'):
content = content.replace('${' + key + '}', os.environ.get(key, ''))
p.write_text(content)
PYEOF

# For the diff, we'll append it separately due to size
echo "" >> /tmp/docs-check-prompt.txt
Expand Down Expand Up @@ -720,8 +770,11 @@ jobs:
SCRIPT_ARGS="$SCRIPT_ARGS --auto-commit"
fi

# Run post-processing script
bun run "$POST_SCRIPT" $SCRIPT_ARGS
# Run post-processing script. BUN_CONFIG_FILE=/dev/null is already set
# at the job env (line 187), but pinning it inline keeps Semgrep's
# bun-run-in-reusable-workflow rule satisfied and survives anyone
# later moving this into a step that overrides the job env.
BUN_CONFIG_FILE=/dev/null bun run "$POST_SCRIPT" $SCRIPT_ARGS

# Check if branch was created
if [ -f /tmp/fixup-branch-name.txt ]; then
Expand Down
26 changes: 25 additions & 1 deletion .github/workflows/_claude-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,12 @@ jobs:

steps:
# FIXED: Security scanning with bullfrog — block mode prevents exfiltration
# This step cannot be disabled or modified
# This step cannot be disabled or modified.
# Per .github/workflows/CLAUDE.md, bullfrog must be the FIRST step in
# every job on a non-macOS runner ("no exceptions"), so it runs before
# the toolkit_ref validation below. Ordering is safe: bullfrog does not
# consume toolkit_ref, and the validation runs before any step that
# downloads action.yml or post-*.ts content using it.
- name: Security scanning
uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3 # v0.8.4
with:
Expand All @@ -308,6 +313,25 @@ jobs:
*.npmjs.org
enable-sudo: false

# Validate toolkit_ref BEFORE any downstream step uses it to download
# action.yml / post-*.ts script content from the ai-toolkit repo.
# Without this, an attacker-controlled ref could substitute malicious
# script content that runs inside this job with access to its secrets.
- name: Validate toolkit_ref
env:
TOOLKIT_REF: ${{ inputs.toolkit_ref }}
run: |
case "$TOOLKIT_REF" in
main|next) ;;
*)
if ! printf '%s' "$TOOLKIT_REF" | grep -qE '^[0-9a-fA-F]{40}$'; then
echo "::error::Invalid toolkit_ref: '$TOOLKIT_REF'. Allowed: main, next, or a full 40-char SHA."
exit 1
fi
;;
esac
echo "✅ toolkit_ref validated: $TOOLKIT_REF"

# FIXED: Checkout with consistent fetch depth
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
71 changes: 68 additions & 3 deletions .github/workflows/_claude-task-worker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,13 @@ on:
description: "PAT for pushing branches (optional, falls back to GITHUB_TOKEN)"
required: false

# Disable Bun's automatic bunfig.toml loading from CWD. `bun run` invocations
# in this workflow run after checkout of the worker's repo; without this,
# a malicious bunfig.toml at the checkout root would have its preload array
# execute arbitrary code before the intended script (RCE with caller secrets).
env:
BUN_CONFIG_FILE: /dev/null

jobs:
process-task:
runs-on: ubuntu-latest
Expand All @@ -179,11 +186,52 @@ jobs:
actions: read

steps:
# Security scanning
# Security scanning — block mode prevents secret exfiltration if RCE is achieved.
#
# Per .github/workflows/CLAUDE.md, bullfrog must be the FIRST step in
# every job on a non-macOS runner ("no exceptions"), so it runs before
# the toolkit_ref validation below. Ordering is safe: bullfrog does not
# consume toolkit_ref, and the validation runs before any step that
# downloads action.yml or post-*.ts content using it.
#
# Allowlist is workflow-specific. github.com / api.github.com are in
# bullfrog's defaultDomains (agent/agent.go:18) and don't need to be
# listed. claude.ai / *.claude.ai apply only to the OAuth token path
# (CLAUDE_CODE_OAUTH_TOKEN); this worker uses the API-key path and
# never resolves them. bun.sh is unused — setup-bun fetches the binary
# from GitHub releases (covered by *.githubusercontent.com), confirmed
# by 15 audit-mode runs of _generate-pr-metadata.yml that use the same
# setup-bun action and showed 0 audit lines for bun.sh.
- name: Security scanning
uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3 # v0.8.4
with:
egress-policy: audit
egress-policy: block
allowed-domains: |
release-assets.githubusercontent.com
api.anthropic.com
api.linear.app
registry.npmjs.org
enable-sudo: false

# Validate toolkit_ref BEFORE any downstream step uses it to download
# action.yml / post-*.ts script content from the ai-toolkit repo.
# Without this, an attacker-controlled ref (e.g., a fork branch passed
# by a caller) could substitute malicious script content that runs
# inside this job's process with access to its secrets.
- name: Validate toolkit_ref
env:
TOOLKIT_REF: ${{ inputs.toolkit_ref }}
run: |
case "$TOOLKIT_REF" in
main|next) ;;
*)
if ! printf '%s' "$TOOLKIT_REF" | grep -qE '^[0-9a-fA-F]{40}$'; then
echo "::error::Invalid toolkit_ref: '$TOOLKIT_REF'. Allowed: main, next, or a full 40-char SHA."
exit 1
fi
;;
esac
echo "✅ toolkit_ref validated: $TOOLKIT_REF"

# Checkout the target branch
# Must come before validate-claude-auth (local composite action)
Expand Down Expand Up @@ -535,10 +583,27 @@ jobs:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
prompt: ${{ steps.build-prompt.outputs.prompt }}
show_full_output: ${{ inputs.debug_mode }}
# Replaced --dangerously-skip-permissions with an explicit allowedTools list
# so Claude doesn't auto-approve every tool call. Without an allowlist
# Claude prompts on every tool call (no human to approve in CI, so the job
# stalls); with --dangerously-skip-permissions a prompt-injected session
# could call any tool.
#
# SECURITY MODEL: the allowlist below includes Bash(node:*), Bash(python3:*),
# Bash(bunx:*), and Bash(npx:*) — each is arbitrary code execution. This
# allowlist alone does NOT contain a hijacked session: a prompt-injected
# Claude can run `node -e "..."` or `npx <malicious-package>` to do anything
# the comment used to claim was "excluded". The actual containment is the
# bullfrog egress block above: even if a hijacked session runs `node -e` to
# exfiltrate, traffic is blocked to non-allowlisted hosts. Do not relax the
# bullfrog allowlist without re-evaluating this layer.
#
# Refine as needed — if a legitimate task fails because a tool is missing,
# add the tool here.
claude_args: |
--model ${{ inputs.model }}
--max-turns 150
--dangerously-skip-permissions
--allowedTools "Read,Write,Edit,MultiEdit,NotebookEdit,Glob,Grep,WebSearch,WebFetch,Bash(git:*),Bash(gh pr:*),Bash(gh issue:*),Bash(gh api:*),Bash(npm:*),Bash(bun:*),Bash(bunx:*),Bash(npx:*),Bash(node:*),Bash(python3:*),Bash(ls:*),Bash(cat:*),Bash(head:*),Bash(tail:*),Bash(grep:*),Bash(rg:*),Bash(find:*),Bash(awk:*),Bash(sed:*),Bash(jq:*),Bash(date:*),Bash(wc:*),Bash(cut:*),Bash(tr:*),Bash(sort:*),Bash(uniq:*),Bash(diff:*),Bash(echo:*),Bash(printf:*),Bash(mkdir:*),Bash(touch:*),Bash(chmod:*),Bash(mv:*),Bash(cp:*),mcp__linear__linear_get_issue,mcp__linear__linear_list_issues,mcp__linear__linear_add_comment,mcp__linear__linear_search_documentation,mcp__linear__linear_update_issue"
--mcp-config '{"mcpServers":{"linear":{"command":"bunx","args":["linear-mcp-server"],"env":{"LINEAR_API_KEY":"${{ secrets.LINEAR_API_KEY }}"}}}}'
plugin_marketplaces: ${{ steps.build-plugins.outputs.plugin_marketplaces }}
plugins: ${{ steps.build-plugins.outputs.plugins }}
Expand Down
47 changes: 45 additions & 2 deletions .github/workflows/_generate-pr-metadata.yml
Original file line number Diff line number Diff line change
Expand Up @@ -172,11 +172,54 @@ jobs:
pull-requests: write # Required to update PR title and description

steps:
# Security scanning (required for all workflows)
# Security scanning — flipped from audit to block.
# Per .github/workflows/CLAUDE.md, bullfrog must be the FIRST step in
# every job on a non-macOS runner ("no exceptions"), so it runs before
# the toolkit_ref validation below. Ordering is safe: bullfrog does not
# consume toolkit_ref, and the validation runs before any step that
# downloads action.yml or post-*.ts content using it.
#
# Audit-mode sampling identified registry.npmjs.org (bun install)
# and release-assets.githubusercontent.com (setup-bun) as the only
# first-resolution destinations, BUT bullfrog v0.8.4 audit only
# queues DNS *replies* (agent/queue_audit.nft); cached resolutions
# emit no further events. Block-mode CI then revealed three more:
# api.anthropic.com (runtime API), raw.githubusercontent.com
# (build-plugin-config fetches marketplace.json), and claude.ai
# (Claude SDK install + runtime telemetry, even on API-key auth).
# github.com / api.github.com are in bullfrog defaultDomains.
# Add destinations here as future block-mode CI failures reveal them.
- name: Security Scan
uses: bullfrogsec/bullfrog@1831f79cce8ad602eef14d2163873f27081ebfb3 # v0.8.4
with:
egress-policy: audit
egress-policy: block
allowed-domains: |
release-assets.githubusercontent.com
raw.githubusercontent.com
api.anthropic.com
claude.ai
downloads.claude.ai
registry.npmjs.org
enable-sudo: false

# Validate toolkit_ref BEFORE any downstream step uses it to download
# action.yml / post-*.ts script content from the ai-toolkit repo.
# Without this, an attacker-controlled ref could substitute malicious
# script content that runs inside this job with access to its secrets.
- name: Validate toolkit_ref
env:
TOOLKIT_REF: ${{ inputs.toolkit_ref }}
run: |
case "$TOOLKIT_REF" in
main|next) ;;
*)
if ! printf '%s' "$TOOLKIT_REF" | grep -qE '^[0-9a-fA-F]{40}$'; then
echo "::error::Invalid toolkit_ref: '$TOOLKIT_REF'. Allowed: main, next, or a full 40-char SHA."
exit 1
fi
;;
esac
echo "✅ toolkit_ref validated: $TOOLKIT_REF"

# Checkout code with full history for pattern analysis
# Must come before validate-claude-auth (local composite action)
Expand Down
Loading
Loading