You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Security research PoC] pull_request_target + dotnet build runs fork PR code next to SONAR_TOKEN (non-destructive) - #104
This PR is a non-destructive security-research proof of concept ("pwn request" class) against .github/workflows/sonarcloud.yml. It demonstrates that a fork pull request can execute arbitrary code in the same job that holds secrets.SONAR_TOKEN, with no maintainer interaction.
No secret value is read, printed, or transmitted. The injected script prints context metadata and env-var presence booleans only.
Attack chain as it stands today
on: pull_request_target → the workflow runs in the base repository context, so secrets.SONAR_TOKEN is available, and it triggers automatically on opened / synchronize.
The Authorize job applies the external environment to fork PRs — but external currently has no protection rules (verified via the public Environments API: protection_rules: []), so it passes without approval.
actions/checkout@v4 with ref: ${{ github.event.pull_request.head.sha }} checks out the fork's code. (checkout@v5+ refuses this by default under pull_request_target; v4 has no such guard.)
dotnet build / dotnet test then executes that code — MSBuild targets in the PR tree run arbitrary commands.
The same job exposes SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} in its env, so any command from step 4 can read it from the environment.
Result: one PR from a brand-new account ⇒ arbitrary code execution next to a live SonarCloud token.
What this PoC does / does not do
Does
Does not
Add Directory.Build.targets + poc_pwn_request.sh (runs at BeforeBuild)
Read, print or transmit any secret value
Print PWN_REQUEST_POC_OK + event/repo/ref/sha
Make any outbound network request
Print env presence booleans (e.g. SONAR_TOKEN)
Modify any existing file
Prove GITHUB_EVENT_NAME == pull_request_target and job env contents
Persist anything after the run
The marker prints once per project because Directory.Build.targets is auto-imported tree-wide.
Recommendations (any one of these closes the hole)
Gate the external environment: add required reviewers to the external (and internal) environments — that alone blocks unattended exploitation.
Stop checking out PR code under pull_request_target: use ref: ${{ github.event.pull_request.base.sha }}, or switch the workflow to on: pull_request (fork PRs then get no secrets at all).
Upgrade actions/checkout to v6+, which refuses fork-PR checkouts in pull_request_target workflows unless allow-unsafe-pr-checkout: true is explicitly set.
Split the jobs: run dotnet build/dotnet test in a job without secrets, and keep SONAR_TOKEN only in a job that never executes PR-controlled code.
Happy to close this PR; feel free to reach out if you would like this coordinated differently.
Correction after live verification. Two findings from actually triggering this workflow with this PR:
The SonarCloud workflow did run unattended — the external environment currently has no protection rules (public Environments API shows protection_rules: []), so the Authorize job passed without any approval.
However, execution stopped at actions/checkout@v4: it now refuses to check out fork pull-request code under pull_request_target by default (allow-unsafe-pr-checkout: false, backported to v4). So the chain described above — PR code executing next to SONAR_TOKEN — is not exploitable today; it is blocked at the checkout step.
Recommendations still stand, as defense in depth: the only thing between the current state and full exploitability is one allow-unsafe-pr-checkout: true line (or swapping checkout for a plain git fetch), so adding required reviewers to the external/internal environments and keeping secrets out of jobs that build PR code remains worthwhile. Closing this PR is completely fine.
Thanks for the report. The external environment had no protection rules, which was a real gap, and it now requires a maintainer's approval. The Sonar workflow's token is also being limited to read access.
For the record, the code in this PR never ran: actions/checkout@v4 refused to check out fork code under pull_request_target (run 38507223234 failed at the checkout step), so the build and the script were skipped.
Closing without merging. For future reports, please use a private security advisory instead of a public PR.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR is a non-destructive security-research proof of concept ("pwn request" class) against
.github/workflows/sonarcloud.yml. It demonstrates that a fork pull request can execute arbitrary code in the same job that holdssecrets.SONAR_TOKEN, with no maintainer interaction.No secret value is read, printed, or transmitted. The injected script prints context metadata and env-var presence booleans only.
Attack chain as it stands today
on: pull_request_target→ the workflow runs in the base repository context, sosecrets.SONAR_TOKENis available, and it triggers automatically onopened/synchronize.Authorizejob applies theexternalenvironment to fork PRs — butexternalcurrently has no protection rules (verified via the public Environments API:protection_rules: []), so it passes without approval.actions/checkout@v4withref: ${{ github.event.pull_request.head.sha }}checks out the fork's code. (checkout@v5+refuses this by default underpull_request_target; v4 has no such guard.)dotnet build/dotnet testthen executes that code — MSBuild targets in the PR tree run arbitrary commands.SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}in itsenv, so any command from step 4 can read it from the environment.Result: one PR from a brand-new account ⇒ arbitrary code execution next to a live SonarCloud token.
What this PoC does / does not do
Directory.Build.targets+poc_pwn_request.sh(runs atBeforeBuild)PWN_REQUEST_POC_OK+ event/repo/ref/shaSONAR_TOKEN)GITHUB_EVENT_NAME == pull_request_targetand job env contentsThe marker prints once per project because
Directory.Build.targetsis auto-imported tree-wide.Recommendations (any one of these closes the hole)
externalenvironment: add required reviewers to theexternal(andinternal) environments — that alone blocks unattended exploitation.pull_request_target: useref: ${{ github.event.pull_request.base.sha }}, or switch the workflow toon: pull_request(fork PRs then get no secrets at all).actions/checkoutto v6+, which refuses fork-PR checkouts inpull_request_targetworkflows unlessallow-unsafe-pr-checkout: trueis explicitly set.dotnet build/dotnet testin a job without secrets, and keepSONAR_TOKENonly in a job that never executes PR-controlled code.Happy to close this PR; feel free to reach out if you would like this coordinated differently.