Safeguard
Open Source Security

Packagist's GitHub Webhook Flaw That Could Have Poisoned ...

A 2022 Packagist webhook vulnerability let a crafted GitHub branch name trigger command injection on Packagist's servers, threatening the entire PHP/Composer supply chain.

Vikram Iyer
Security Researcher
6 min read

In April 2022, a single crafted branch name was all it would have taken to compromise the software supply chain for millions of PHP applications. Security researchers at Sonar disclosed a Packagist webhook vulnerability that sat inside the update pipeline every PHP developer relies on without ever thinking about it: the automated process that fires every time you push code to GitHub and Packagist quietly re-indexes your package. Tracked as CVE-2022-24828, the flaw let an attacker who controlled a Git or Mercurial repository execute arbitrary commands on Packagist's own servers the moment that repository was pulled for metadata. Packagist serves roughly 2 billion downloads a month through Composer. Had this been exploited before Sonar's disclosure, an attacker wouldn't have needed to compromise a maintainer's account or steal an npm-style publish token — they'd only have needed a GitHub repo and a well-placed dash.

What was the Packagist webhook vulnerability?

The Packagist webhook vulnerability, cataloged as CVE-2022-24828, was a command injection flaw in the Composer code that Packagist uses to read package metadata from Git and Mercurial repositories. When Packagist needs to refresh a package's information, it hands the repository's URL and branch name to Composer's VcsDriver::getFileContents method, which in turn shells out to git or hg to fetch a file — commonly the README referenced in composer.json. Sonar's researchers found that a branch name beginning with a dash (-) would be interpreted by the underlying git or hg binary as a command-line flag rather than a literal ref name, because Composer's code, unlike the version fixed after 2021's CVE-2021-29472, no longer inserted the -- argument-separator that normally tells command-line tools "everything after this is data, not options." That gap let a specially named branch smuggle arbitrary parameters into the command Packagist executed on its own infrastructure.

How did the GitHub webhook trigger the flaw?

It triggered through the exact mechanism Packagist tells every maintainer to set up: a GitHub webhook pointed at Packagist's update API. Packagist's standard onboarding instructs you to add a webhook on your GitHub repository that pings packagist.org/api/github (or the generic api/update-package endpoint) on every push. That webhook call doesn't carry code — it's just a signal that says "something changed, go pull the latest metadata." Packagist's asynchronous workers pick up that signal and clone or fetch the repository to re-read composer.json. That fetch step is precisely where the vulnerable VcsDriver code ran. In other words, the GitHub webhook wasn't misconfigured or spoofed — it worked exactly as designed. The danger was that "as designed" meant any repository owner, including a malicious one who published a package to Packagist, could get their crafted branch name executed on Packagist's backend the instant the webhook fired.

Why could this have poisoned PHP packages distributed through Composer?

Because the injection ran on Packagist's own servers, not on a developer's machine, meaning a single successful exploit could have touched every package Packagist serves, not just the attacker's own. Composer, the PHP dependency manager that reads Packagist's index, resolves and downloads packages based entirely on the metadata Packagist publishes. If an attacker had used the flaw to achieve remote code execution on Packagist's backend, they could have altered the metadata, download URLs, or version pointers for any package in the registry — not merely the one they uploaded to stage the attack. Sonar estimated the flaw could have been used to hijack "over 100 million requests" for malicious dependencies, given Composer's install volume. That's the defining feature of a PHP supply chain attack of this kind: it doesn't need to trick a developer into typing composer require evil/package; it can quietly rewrite what a trusted, already-installed package resolves to.

Who found the flaw, and how fast did Packagist fix it?

Sonar's research team reported the vulnerability to Packagist on April 7, 2022, and Packagist deployed a hotfix to both Packagist.org and Private Packagist within 24 hours, by April 8. Composer itself shipped patched releases — 2.3.5, 2.2.12, and 1.10.26 — shortly after, so that self-hosted Composer/Satis-style deployments running their own VCS-driver code were also covered. Packagist's team subsequently reviewed logs and audited its database for signs of prior abuse and stated it found no evidence the vulnerability had been exploited in the wild before the disclosure. The fix itself was narrow and unglamorous: reject branch and tag names that start with a dash before they ever reach the git or hg command line, closing the same class of argument-injection issue that had already been partially patched a year earlier under CVE-2021-29472.

What does this reveal about GitHub webhook and registry trust models generally?

It reveals that automated update pipelines — the GitHub webhook hooks, cron jobs, and async workers that keep package registries fresh — are themselves part of the attack surface, even when no human account is compromised. Composer, npm, PyPI, RubyGems, and every other ecosystem that auto-syncs metadata from a VCS host share the same basic shape: a webhook or poller notifies a backend service, the backend service fetches untrusted, attacker-influenced content, and that content is parsed by code the registry maintainers wrote and trusted. The Packagist case is a clean illustration because the trigger (a GitHub webhook), the untrusted input (a branch name), and the privileged execution context (Packagist's own servers) were all ordinary, well-documented parts of the system. Nothing had to be tricked or social-engineered. That's exactly the pattern security teams should be hunting for across every registry integration they depend on, not just Packagist: where does automatically-fetched, attacker-controllable metadata get handed to a shell, a parser, or a deserializer running with elevated trust?

How Safeguard Helps

Safeguard is built for exactly this class of risk — the ones that live in the plumbing between your source repositories and the package registries your builds pull from, not just in flagged CVEs after the fact. For teams running Composer, npm, PyPI, or other package ecosystems, Safeguard continuously monitors your software supply chain for anomalous package behavior, unexpected metadata changes, and dependency resolution that doesn't match what your source control actually shipped — the kind of divergence a Packagist webhook vulnerability like CVE-2022-24828 could have introduced. Safeguard also audits the GitHub webhook and CI/CD integrations your organization relies on to auto-publish and auto-update packages, surfacing overly trusted execution paths, missing input validation, and third-party registry dependencies that could become single points of failure across your entire PHP supply chain or any other language ecosystem. Rather than waiting for the next disclosure to find out an upstream registry had backend command execution reachable through routine automation, Safeguard's continuous monitoring flags suspicious package version changes, unusual publish timing, and integrity mismatches between what your webhook triggers and what actually lands in your dependency tree — giving security teams a real-time signal instead of a retrospective advisory. When registries you don't control sit in your critical path, Safeguard is the layer that verifies what's actually being pulled into your builds matches what your developers intended, whether the vulnerability lives in a dependency, a registry's ingestion pipeline, or the webhook wiring in between.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.