CVE Tools
Back to blog

CVE-2026-44359: how one pull request could hijack Meshtastic's build pipeline

A CVSS 10 GitHub Actions "pwn request" -- and the pattern that keeps compromising open-source supply chains

CVE-2026-44359: how one pull request could hijack Meshtastic's build pipeline. A CVSS 10 GitHub Actions "pwn request" -- and the pattern that keeps compromising open-source supply chains. meshtastic/f
CVE-2026-44359: how one pull request could hijack Meshtastic's build pipeline. A CVSS 10 GitHub Actions "pwn request" -- and the pattern that keeps compromising open-source supply chains. meshtastic/f

A single pull request -- from anyone with a GitHub account -- could have handed an attacker Meshtastic's firmware signing keys, its self-hosted build servers, and write access to the master branch. That is CVE-2026-44359, a CVSS 10.0 flaw in the meshtastic/firmware GitHub Actions pipeline, and it is a textbook example of the CI/CD "pwn request" that keeps compromising open-source supply chains.

What actually happened

The project's main_matrix.yml workflow was triggered by the pull_request_target event. Unlike the ordinary pull_request trigger, pull_request_target runs in the base repository's trust context -- with access to repository secrets and a write-capable GITHUB_TOKEN. The workflow then checked out the pull request author's fork and ran its build and test scripts across four separate jobs, with no approval gate. Pull requests from complete strangers (author_association: "NONE") fired the pipeline automatically.

Why `pull_request_target` is a loaded gun

GitHub built pull_request_target so that trusted automation -- labelling a PR, posting a first-response comment -- can run even on contributions from forks. The catch is that it runs with the base repo's privileges. The moment a workflow using that trigger also checks out the untrusted PR head (ref: ${{ github.event.pull_request.head.ref }}) and executes it, every secret in that job is exposed to whoever opened the PR. GitHub's own Security Lab calls the pattern a "pwn request" and flatly warns that it "may lead to repository compromise." Because the attacker needs no privileges and no user interaction, the flaw scores the maximum 10.0 (CWE-94 and CWE-829).

The exploitation chain

The attack has no memory corruption, no authentication bypass -- just four steps: fork the repo, swap a script the CI will run (say bin/check-all.sh) for a payload that reads the secrets, open a pull request, and let the pipeline run it. From there it branches into three distinct compromises. The chain below maps it end to end, with the chokepoints where a defender can prevent or detect it:

CVE-2026-44359 exploitation chain -- fork PR to supply-chain compromise

  1. Attacker forks meshtastic/firmware — Any GitHub account. No privileges needed.
  2. Swap a CI-run build script in the fork — e.g. **bin/check-all.sh** with a secret-exfil payload.
  3. Open a pull request — author_association: NONE.
  4. pull_request_target auto-runs -- NO approval — Runs in BASE context: secrets + write GITHUB_TOKEN.
  5. Steal PPA_GPG_PRIVATE_KEY — Sign malicious .deb served over apt.
  6. Own the self-hosted build runners — Persistent backdoor; pivot internally.
  7. Write GITHUB_TOKEN: push to master — Modify releases; grab DIST_PAGES_DEPLOY_KEY.
  8. Supply-chain compromise: poisoned builds to users — Signed malware reaches off-grid mesh devices.
  9. Prevent: pull_request / workflow_run split — Never run fork code under a privileged trigger.
  10. Detect: Harden-Runner egress / fork-PR alert — Unexpected outbound from a build job = exfil.

The four jobs and what each one leaks

The advisory enumerates four vulnerable jobs in the workflow. Each is a different blast radius, and together they add up to full supply-chain control:

JobWhat it exposesImpact
build-debian-srcPPA_GPG_PRIVATE_KEY (runs mk-build-deps on attacker debian/control)Sign malicious .deb packages served over apt
checkSelf-hosted "arctastic" runners (runs check-all.sh from the fork)Persistent runner backdoor; internal pivot
buildSelf-hosted arctastic runnersSame -- code execution on persistent infra
gather-artifactsGITHUB_TOKEN with contents:write + pull-requests:writePush to master; modify releases; repo takeover

Am I affected?

Meshtastic itself is patched -- but the pattern is the real story, because it is everywhere. Ask two questions of your own repositories:

  • Do any workflows trigger on pull_request_target? If not, you are not exposed to this class.
  • Do those workflows check out the PR head (github.event.pull_request.head.sha / .head.ref / refs/pull/N/merge) or run fork build scripts? If yes, treat it as an emergency: those runs execute untrusted code with your secrets.
  • Meshtastic users and integrators: update to firmware 2.7.21.1370b23 or later and only trust binaries from the official post-fix release.

How we got here

Disclosure timeline

  1. CVE-2025-53637 fixed in 2.6.6
    A command-injection in the same main_matrix.yml (attacker-controlled branch name) is patched -- but only in the setup job.
  2. CVE-2026-44359 disclosed
    Reporter avivdon shows the deeper problem: the fork-checkout execution across the check, build and build-debian-src jobs was never fixed.
  3. CISA-ADP marks Exploitation = PoC
    A proof-of-concept video exists; still no observed in-the-wild abuse of the repo.
  4. GitHub hardens actions/checkout v7
    Checkout v7 refuses fork-PR code under pull_request_target by default -- an ecosystem-wide response to this class.
  5. NVD publishes CVE-2026-44359
    CVSS 10.0. Not on CISA KEV.

Find it in your own repos

You do not need a security vendor to check for this. A two-condition sweep -- workflows that are pull_request_target and check out the PR head -- keeps false positives low. Drop the following into CI as a merge gate; it exits non-zero the moment a dangerous workflow appears:

Detections for the pull_request_target pwn-request pattern3 rules
other ×3

Author-written CI gate plus off-the-shelf analyzers. All are deployable as-is.

otherCI gate: fail on pull_request_target + untrusted checkout (author-written)
Workflows that both trigger on pull_request_target and check out the PR head ref / run fork scripts bash + grep in the repo checkout
#!/usr/bin/env bash
# fail-on-pwn-request.sh -- exit 1 if a dangerous workflow is found
set -euo pipefail
hits=0
for f in $(grep -rEl 'pull_request_target' .github/workflows/ 2>/dev/null); do
  if grep -Eq 'actions/checkout' "$f" && \
     grep -Eq 'github\.event\.pull_request\.head\.(sha|ref)|refs/pull/[^ ]*/merge|github\.head_ref' "$f"; then
     echo "DANGEROUS pwn-request pattern: $f"; hits=1
  fi
done
[ "$hits" -eq 0 ] || { echo 'pull_request_target + untrusted PR checkout detected'; exit 1; }
otherzizmor -- purpose-built GitHub Actions auditor (SARIF for code scanning)
dangerous-triggers (pull_request_target/workflow_run) and template-injection
zizmor .github/workflows/            # local audit
zizmor --format=sarif .github/workflows/ > zizmor.sarif   # CI gate
otherHarden-Runner -- runtime egress monitoring (detect secret exfil)
Anomalous outbound network from a CI job -- the behaviour secret exfiltration produces
- uses: step-security/harden-runner@v2
  with:
    egress-policy: audit   # move to block + allowed-endpoints once baselined

At fleet scale, Poutine (BoostSecurity) and Gato-X (Praetorian) classify pwn requests and self-hosted-runner takeover across whole orgs with a single token.

How to fix it -- properly

The fix is architectural, not a version bump. Sanitising one input just moves the problem, which is exactly how Meshtastic ended up patching the same workflow twice:

  1. Do not run untrusted code under pull_request_target. Build and test forked PRs with the ordinary pull_request trigger (no secrets, read-only token).
  2. If you need base-context privileges, split the work: an unprivileged pull_request workflow builds and uploads an artifact; a privileged workflow_run workflow consumes it and does the sensitive step -- without ever checking out fork code.
  3. Require approval for fork runs -- pick GitHub's strongest option, "Require approval for all outside collaborators."
  4. Minimise the token: default-deny permissions: {} at the top of every workflow, grant more only per job.
  5. Pin third-party actions by full commit SHA, not a moving tag.
  6. Upgrade to actions/checkout@v7, which refuses fork-PR checkouts under pull_request_target by default.

Why this keeps happening

CVE-2026-44359 is one point on a multi-year curve of CI/CD supply-chain compromise. The tj-actions/changed-files incident (CVE-2025-30066) leaked CI secrets from ~23,000 repositories into public logs; the Nx "s1ngularity" attack abused pull_request_target to steal an npm publish token; mass campaigns like GhostAction and Megalodon industrialised CI-token theft across thousands of repos. A pipeline is a concentrated trust boundary -- one workflow holds your signing keys, deploy keys and write tokens -- and pull_request_target hands that trust to anyone who can open a pull request. Audit your pipeline with the same rigour as your production code.

Is CVE-2026-44359 a command-injection vulnerability?
No. The GitHub advisory states the workflow directly executes attacker-controlled files from the fork checkout -- there is no injection step. That distinguishes it from the earlier CVE-2025-53637 in the same workflow, which was a branch-name command injection.
Is it being exploited in the wild?
There is a proof-of-concept video and a CISA-ADP "PoC" flag, but no evidence of real-world exploitation of the Meshtastic repository as of 20 Jul 2026, and it is not on CISA KEV. The repo was patched before any known abuse.
I don't use Meshtastic -- does this affect me?
Only if your own GitHub workflows combine the pull_request_target trigger with a checkout of the pull request head (or execution of fork scripts). If they do, the risk is the same class and you should remediate immediately. If they don't, you're not exposed.
What is the fix version?
Meshtastic firmware 2.7.21.1370b23, via a commit that converts the workflow from pull_request_target to pull_request and removes the fork-checkout overrides.

Sources