Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"id": "d3a8b4c1-f2e7-4a9b-8c5d-1e6f0a2b3c4d",
"queryName": "Beta - Checkout Of Untrusted Code In Privileged Context",
"severity": "HIGH",
"category": "Insecure Configurations",
"descriptionText": "A workflow triggered by pull_request_target runs with the target repository's secrets and write permissions. If such a workflow checks out the PR contributor's code (e.g. github.event.pull_request.head.sha or github.head_ref) and then executes it, an attacker can submit a malicious PR to run arbitrary code with elevated privileges. This is known as a 'pwn request'. Fix: check out only the trusted base branch ref (e.g. github.event.pull_request.base.sha) in the privileged job.",
"descriptionUrl": "https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/",
"platform": "CICD",
"descriptionID": "b1c2d3e4",
"cloudProvider": "common",
"cwe": "829",
"oldSeverity": "HIGH",
"riskScore": "9.0",
"experimental": "true"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package Cx

import data.generic.common as common_lib

# Detect checkout of attacker-controlled code in a pull_request_target workflow.
#
# pull_request_target runs with the target repo's secrets and write permissions.
# Checking out the PR contributor's branch/commit (head ref) and then executing
# any code from that checkout gives the attacker elevated access ("pwn request").
#
# Safe pattern: check out only the base branch ref (pull_request.base.sha / base.ref).
# Unsafe patterns include: pull_request.head.sha, pull_request.head.ref, head_ref, etc.

CxPolicy[result] {
input.document[i].on["pull_request_target"]

step := input.document[i].jobs[j].steps[k]
startswith(step.uses, "actions/checkout")

ref := step["with"].ref
isUntrustedHeadRef(ref)

result := {
"documentId": input.document[i].id,
"searchKey": sprintf("jobs.%s.steps.with.ref={{%s}}", [j, ref]),
"issueType": "IncorrectValue",
"keyExpectedValue": "Checkout uses a trusted base branch ref (e.g. github.event.pull_request.base.sha) in a pull_request_target workflow.",
"keyActualValue": "Checkout uses an untrusted PR head ref in a pull_request_target workflow, allowing execution of attacker-controlled code with elevated permissions.",
"searchLine": common_lib.build_search_line(["jobs", j, "steps", k, "with", "ref"], []),
}
}

# Matches any ref expression that references the PR head (attacker-controlled):
# github.event.pull_request.head.* (e.g. .sha, .ref, .label, .repo.*)
# github.head_ref

isUntrustedHeadRef(ref) {
regex.match("github\\.event\\.pull_request\\.head", ref)
}

isUntrustedHeadRef(ref) {
regex.match("github\\.head_ref", ref)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: PR Quality Checks

on:
pull_request_target:
types: [opened, edited, synchronize, reopened]

permissions:
contents: read
pull-requests: read

jobs:
detect:
name: Detect PR type
runs-on: ubuntu-latest
outputs:
is_package_pr: ${{ steps.check.outputs.is_package_pr }}
steps:
- name: Check if README.md is modified
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
files=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files --jq '.[].filename' 2>/dev/null || echo "")
if echo "$files" | grep -q '^README.md$'; then
echo "is_package_pr=true" >> "$GITHUB_OUTPUT"
echo "README.md is modified — this is a package PR"
else
echo "is_package_pr=false" >> "$GITHUB_OUTPUT"
echo "README.md not modified — skipping quality checks"
fi

quality:
name: Repository quality checks
needs: detect
if: needs.detect.outputs.is_package_pr == 'true'
runs-on: ubuntu-latest
environment: action
container: golang:latest
permissions:
contents: read
pull-requests: read
outputs:
comment: ${{ steps.quality.outputs.comment }}
labels: ${{ steps.quality.outputs.labels }}
fail: ${{ steps.quality.outputs.fail }}
diff_comment: ${{ steps.diff.outputs.diff_comment }}
diff_fail: ${{ steps.diff.outputs.diff_fail }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
fetch-depth: 0

- name: Fetch base branch and PR head
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
AUTH="$(printf '%s' "x-access-token:${GITHUB_TOKEN}" | base64 -w0)"
git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${AUTH}" fetch origin "${{ github.base_ref }}"
git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic ${AUTH}" fetch origin "+refs/pull/${{ github.event.pull_request.number }}/head"

- name: Run quality checks
id: quality
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: go run ./.github/scripts/check-quality/

- name: Run diff checks
id: diff
continue-on-error: true
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: go run ./.github/scripts/check-pr-diff/

report:
name: Post quality report
needs: [detect, quality]
if: always() && needs.detect.outputs.is_package_pr == 'true' && needs.quality.result != 'cancelled'
runs-on: ubuntu-latest
permissions:
pull-requests: write
contents: write
steps:
- name: Post quality report comment
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-quality-check
message: |
${{ needs.quality.outputs.comment }}

---

${{ needs.quality.outputs.diff_comment }}

- name: Sync labels
if: needs.quality.outputs.labels != ''
uses: actions-ecosystem/action-add-labels@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: ${{ join(fromJson(needs.quality.outputs.labels), '\n') }}

- name: Fail if critical checks failed
if: needs.quality.outputs.fail == 'true' || needs.quality.outputs.diff_fail == 'true'
run: |
echo "Quality or diff checks failed."
exit 1

skip-notice:
name: Skip quality checks (non-package PR)
needs: detect
if: needs.detect.outputs.is_package_pr == 'false'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Post skip notice
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-quality-check
message: |
## Automated Quality Checks

**Skipped** — this PR does not modify `README.md`, so package quality checks do not apply.

_This is expected for maintenance, documentation, or workflow PRs._

auto-merge:
name: Enable auto-merge
needs: [quality, report]
if: always() && needs.quality.result == 'success' && needs.report.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Enable auto-merge via squash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr merge ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--auto \
--squash
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Safe pattern: uses pull_request (not pull_request_target).
# The regular pull_request trigger runs in an isolated environment without
# access to target-repo secrets, so checking out the PR head is safe.
# Additionally, the job condition restricts to same-repo PRs only.

name: Amber Automatic Code Review

on:
pull_request:
types: [opened, synchronize]

jobs:
amber-review:
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
issues: write
id-token: write
actions: read

steps:
- name: Checkout base ref (for command file security)
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.base.ref }}
fetch-depth: 1
path: base-ref

- name: Checkout PR code
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Run code review
uses: anthropics/claude-code-action@v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
github_token: ${{ secrets.GITHUB_TOKEN }}
prompt: |
Review the PR code and post findings as a comment.
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
name: PR Quality Checks

on:
pull_request_target:
types: [opened, edited, synchronize, reopened]

permissions:
pull-requests: write
contents: write

jobs:
detect:
name: Detect PR type
runs-on: ubuntu-latest
outputs:
is_package_pr: ${{ steps.check.outputs.is_package_pr }}
steps:
- name: Check if README.md is modified
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
files=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files --jq '.[].filename' 2>/dev/null || echo "")
if echo "$files" | grep -q '^README.md$'; then
echo "is_package_pr=true" >> "$GITHUB_OUTPUT"
echo "README.md is modified — this is a package PR"
else
echo "is_package_pr=false" >> "$GITHUB_OUTPUT"
echo "README.md not modified — skipping quality checks"
fi

quality:
name: Repository quality checks
needs: detect
if: needs.detect.outputs.is_package_pr == 'true'
runs-on: ubuntu-latest
environment: action
container: golang:latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

- name: Fetch base branch
run: |
git config --global --add safe.directory "$GITHUB_WORKSPACE"
git fetch origin ${{ github.base_ref }}

- name: Run quality checks
id: quality
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: go run ./.github/scripts/check-quality/

- name: Run diff checks
id: diff
continue-on-error: true
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: go run ./.github/scripts/check-pr-diff/

- name: Post quality report comment
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-quality-check
message: |
${{ steps.quality.outputs.comment }}

---

${{ steps.diff.outputs.diff_comment }}

- name: Sync labels
uses: actions-ecosystem/action-add-labels@v1
if: ${{ steps.quality.outputs.labels != '' }}
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
labels: ${{ join(fromJson(steps.quality.outputs.labels), '\n') }}

- name: Fail if critical checks failed
if: ${{ steps.quality.outputs.fail == 'true' || steps.diff.outputs.diff_fail == 'true' }}
run: |
echo "Quality or diff checks failed."
exit 1

skip-notice:
name: Skip quality checks (non-package PR)
needs: detect
if: needs.detect.outputs.is_package_pr == 'false'
runs-on: ubuntu-latest
steps:
- name: Post skip notice
uses: marocchino/sticky-pull-request-comment@v2
with:
header: pr-quality-check
message: |
## Automated Quality Checks

**Skipped** — this PR does not modify `README.md`, so package quality checks do not apply.

_This is expected for maintenance, documentation, or workflow PRs._

auto-merge:
name: Enable auto-merge
needs: quality
if: always() && needs.quality.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Enable auto-merge via squash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh pr merge ${{ github.event.pull_request.number }} \
--repo ${{ github.repository }} \
--auto \
--squash
Loading
Loading