Skip to content

fix(ci): stop executing fork code in check-scope job#559

Merged
Davidnet merged 2 commits into
dataiku:mainfrom
rhiannalitchfield:security/fix-pwn-request-check-scope
Jul 3, 2026
Merged

fix(ci): stop executing fork code in check-scope job#559
Davidnet merged 2 commits into
dataiku:mainfrom
rhiannalitchfield:security/fix-pwn-request-check-scope

Conversation

@rhiannalitchfield

Copy link
Copy Markdown
Contributor

Purpose

Resolves #554. The check-scope job runs under pull_request_target carrying the base repo's GITHUB_TOKEN (with pull-requests: write), but checked out and executed fork-controlled code via ref: ${{ github.event.pull_request.head.sha }}. A malicious fork PR could replace .github/scripts/classify-pr-files.sh to exfiltrate the token or abuse write permissions (classic "pwn request").

This was also surfacing as a functional break: actions/checkout@v7 (#551) now refuses to check out fork PR code by default under pull_request_target.

What Changed

  • check-scope now checks out the trusted base ref (omits ref:), so the trusted copy of classify-pr-files.sh runs.
  • Added a git fetch origin "$HEAD_SHA" step to pull the PR head's objects as data only — never checked out, never executed.
  • classify-pr-files.sh already only computes git diff BASE_SHA...HEAD_SHA --name-only, so it needs the head SHA's objects, not the fork's working tree or any executable code from it.

This follows the fix explicitly recommended in the issue (option 1) and avoids the discouraged allow-unsafe-pr-checkout: true workaround.

Testing

CI-only change; verified the new git fetch + diff flow reproduces the same file list the previous checkout-based diff produced, without checking out or executing fork content.

The check-scope job ran under pull_request_target with the base
repo's GITHUB_TOKEN, but checked out and executed fork-controlled
code (ref: head.sha), letting a malicious PR replace
classify-pr-files.sh to exfiltrate the token or abuse write access.

Check out the trusted base branch instead and fetch the PR head SHA
as data only; classify-pr-files.sh already diffs by SHA and never
needs the fork's working tree.
@rhiannalitchfield

Copy link
Copy Markdown
Contributor Author

Note on the failing "Check PR scope" check: this is expected and actually demonstrates the bug this PR fixes, not a problem with the fix itself.

`pull_request_target` workflows always run the workflow YAML from the base branch (`main`), not from the PR branch, as a security measure. Since this fix only exists on this branch, the check-scope job here still ran the old, vulnerable version of `semantic-pr.yml` (the one with `ref: head.sha`). The log confirms it:

```
Checkout PR
ref: dd3a1dd # this PR's head SHA
##[error]Refusing to check out fork pull request code from a 'pull_request_target' workflow...
```

That's `actions/checkout@v7` blocking the exact unsafe fork checkout described in #554. Once this PR merges, future PRs will run the corrected workflow (checkout base + `git fetch` head as data only) and this failure mode goes away.

uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: ${{ github.event.pull_request.head.sha }}
fetch-depth: 0

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider disabling checkout credential persistence here. This job only needs to fetch and diff, and the later comment action can use its own token context.

Suggested change
fetch-depth: 0
fetch-depth: 0
persist-credentials: false

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 67548fa. Agreed, nothing in this job needs the persisted token: the head fetch works anonymously on a public repo and the sticky-comment action takes its token via its own input.

Comment thread .github/workflows/semantic-pr.yml Outdated
Comment on lines +82 to +85
- name: Fetch PR head
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: git fetch origin "$HEAD_SHA"

@Davidnet Davidnet Jul 2, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

actions/checkout@v7 is intentionally blocking the old pwn-request pattern, so I would avoid any suggestion that opts back into checking out fork-controlled code. A safer pattern is: keep checkout on trusted base code, fetch the PR ref with plain git as data only, verify it still matches the event SHA, and continue running the trusted base copy of classify-pr-files.sh.

Suggested change
- name: Fetch PR head
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: git fetch origin "$HEAD_SHA"
- name: Fetch PR head (data only)
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
# Fetch the advertised PR head ref as data only. This does not check
# out or execute fork-controlled code, preserving the checkout@v7
# pull_request_target pwn-request protection.
git fetch --no-tags --no-recurse-submodules origin \
"refs/pull/${PR_NUMBER}/head"
fetched_sha="$(git rev-parse FETCH_HEAD)"
# Ensure the ref still points at the SHA from the event payload, so we
# classify exactly the reviewed head and never whatever a fork may
# have pushed after the workflow was triggered.
if [ "$fetched_sha" != "$HEAD_SHA" ]; then
echo "::error::PR head ref ($fetched_sha) does not match event head.sha ($HEAD_SHA)."
exit 1
fi

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Applied in 67548fa, took your suggestion essentially verbatim. Fetching the advertised refs/pull/N/head is more robust than relying on unadvertised SHA fetches, and the head.sha verification closes the race where a fork pushes after the trigger. Failing closed there is fine since the new push fires a fresh synchronize run. Note the check-scope run on this PR itself will still show the old failure since pull_request_target takes the workflow from main.

@Davidnet Davidnet left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Let me know what do you think and if the suggestions are clear, but happy to merge if you can tackle the suggestions

Apply review suggestions: disable persist-credentials on the trusted
base checkout, fetch the advertised refs/pull/N/head instead of a bare
SHA, and verify the fetched SHA matches the event payload head.sha so
the job fails closed if the fork pushes after the workflow triggers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Davidnet Davidnet merged commit 0eb825e into dataiku:main Jul 3, 2026
7 of 8 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 3, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

security: check-scope job runs fork code under pull_request_target (pwn request)

2 participants