Executive Summary
On March 3, 2026, Xygeni detected suspicious activity affecting the repository used to publish the xygeni/xygeni-action GitHub Action. The activity originated from compromised credentials associated with a maintainer token and a GitHub App installed on the repository.
During the incident, an attacker attempted to introduce malicious code into the repository through a series of pull requests. These attempts were blocked by existing branch protection rules, preventing the code from being merged into the repository’s main branch.
However, the attacker subsequently exploited a separate vector by moving the mutable v5 tag to reference a malicious commit created during the pull request attempts. Workflows referencing xygeni/xygeni-action@v5 could therefore retrieve the compromised code without any visible change to their workflow definitions.
The tag manipulation was identified on March 9 following community reports. The compromised tag was immediately removed, and incident response procedures were initiated.
Our investigation determined the following:
- No malicious code was merged into the repository’s main branch.
- There is no evidence of compromise of the Xygeni SaaS platform or customer data.
- The exposure window was limited to workflows referencing xygeni/xygeni-action@v5 between March 3 and March 9, 2026.
- The compromised tag has been permanently removed and will not be recreated.
Following the discovery of the tag manipulation, Xygeni implemented multiple security improvements across its repositories and release processes, including:
- Removal of the compromised tag and migration guidance to SHA-pinned references.
- Enforcement of release immutability across repositories.
- Hardening of repository permissions and contributor access.
- Mandatory cryptographically signed commits for maintainers.
- Restriction of write access to a limited set of maintainers and administrators.
We are publishing this report to provide transparency into the incident, share the lessons learned, and help strengthen security practices across the GitHub Actions ecosystem.
While the attack leveraged a known GitHub Actions vulnerability involving mutable tags, the incident also highlights the importance of comprehensive repository protection, strict credential management, and defense-in-depth across CI/CD systems.
Transparency and collaboration are essential to improving the resilience of the software supply chain.
Incident Overview
This report documents the investigation into a security incident affecting the public xygeni/xygeni-action GitHub Action repository.
On March 3, 2026, an attacker used compromised credentials associated with repository automation to introduce malicious code through a series of pull request attempts. The payload contained a command-and-control implant disguised as scanner version telemetry.
Repository branch protection rules successfully prevented the malicious code from being merged into the main branch. However, the attacker later manipulated the mutable v5 tag, redirecting it to a commit containing the injected payload. Because many workflows reference GitHub Actions using major version tags, this tag poisoning allowed downstream workflows referencing xygeni/xygeni-action@v5 to retrieve the compromised code without any visible change to their workflow configuration.
As a provider of software supply chain security tooling, Xygeni operates infrastructure that integrates directly into development pipelines and CI/CD environments. Projects in this space are frequent targets of attacks that aim to compromise widely used developer tooling to reach downstream environments.
Timeline
Phase 1: The Malicious Pull Requests (March 3, 10:22–10:50 UTC)
At 10:22 UTC on March 3, 2026, an attacker began a rapid, coordinated assault on the xygeni-action repository using two compromised identities: a maintainer’s personal access token (PAT) and the private key of a GitHub App (xygeni-onboarding-app-dev). Over the next 28 minutes, three pull requests were created and closed, each injecting obfuscated shell code into action.yml.
The attacker’s approach was methodical and adaptive:
- PR #46 (10:22–10:29 UTC): Created by the compromised maintainer PAT under the branch feat/scanner-telemetry. The attacker attempted to merge but was blocked by branch protection rules. The PR was closed without a merge.
- Reconnaissance (10:39–10:40 UTC): The attacker tested the GitHub App’s access by creating and immediately deleting a branch named test-app-access. This confirmed the App had write access to the repository.
- PR #47 (10:41–10:44 UTC): A second attempt using the maintainer PAT, now under a renamed branch improvement/scanner-telemetry. Again blocked by branch protection. The attacker tried to use the GitHub App to approve the PR — bypassing the two-reviewer requirement by having one compromised identity approve the other’s work. This also failed.
- PR #48 (10:45–10:49 UTC): The roles were swapped — the GitHub App created the PR (pushing commit 4bf1d4e), and the maintainer PAT submitted the approval review. This, too, was rejected by branch protection.
None of the PRs reached the main branch. Our branch protection rules held: the requirement for two approvals, the rule that the most recent push must be approved by someone other than the pusher, and the restriction on bypassing these settings, all combined to block every merge attempt.
Our team detected the anomalous activity during routine PR review and initiated an incident response at 12:21 UTC — less than two hours after the first malicious PR. The response included removing workflows, preserving malicious code for forensic analysis, and securing the repository.
Phase 2: The Tag Poisoning
While branch protection successfully prevented the malicious code from reaching the main. However, the attacker leveraged a separate vector. Using the compromised GitHub App credentials, the attacker moved the mutable v5 tag to point at commit 4bf1d4e — the malicious commit from PR #48 that still existed in the repository’s object store even after the PR branch was deleted.
Critically, this tag manipulation did not happen immediately after the PRs. GitHub’s repository activity logs do not surface tag force-push events in the same way as branch operations, which limits the ability to reconstruct the exact timestamp of the tag modification. However, the tag was confirmed poisoned when the community raised the alarm on March 9.
This is the key insight: branch protection rules do not protect tags. The commit containing the backdoor existed in the repository’s git object database, and the v5 tag — which downstream workflows referenced — could be silently redirected to it. Any workflow using xygeni/xygeni-action@v5 would pull the compromised code, without any change visible in the main branch or in the consuming repositories’ workflow files.
Root Cause
Our investigation concluded that the root cause was the compromise of the private key of a GitHub App (xygeni-onboarding-app-dev) that had been installed on the repository.
This GitHub App was originally created for testing the onboarding experience on Xygeni’s platform. It had write permissions on the repository — permissions that, in hindsight, were broader than necessary for its intended purpose.
With a GitHub App’s private key, an attacker can:
- Generate short-lived installation tokens at will
- Create and approve pull requests
- Push commits via Git over HTTPS
- Move tags — the critical action that made this incident impactful
The attacker used both the compromised maintainer PAT and the GitHub App’s credentials in a coordinated attempt: when one identity couldn’t bypass protections alone, the two identities were used in tandem — one to create, the other to approve.
The exact vector by which the private key was exfiltrated remains under investigation. GitHub App private keys (.pem files) can leak through misconfigured workflows, compromised developer machines, or insecure secret storage.
Malicious Payload Behavior
The injected code was a compact command-and-control implant. It was designed to run silently alongside the legitimate scanner, executing in three phases:
- Registration. The implant contacts a C2 server at 91.214.78.178 (disguised via nip.io wildcard DNS as security-verify.91.214.78.178.nip.io), sending the runner’s hostname, username, and OS version.
- Polling loop. For 180 seconds (fitting within typical CI job timeouts), the implant polls the C2 server every 2–7 seconds for commands to execute.
- Command execution. Any received commands are executed via eval, with output compressed (zlib), base64-encoded, and exfiltrated back to the C2 server.
silently swallowed, variable names were deliberately terse, and the polling interval was randomized to evade traffic pattern detection.
If the implant ran on a CI runner, the attacker would have had access to GITHUB_TOKEN, repository secrets, source code, and potentially artifact signing keys. The implant could have allowed command execution on a CI runner if executed within a workflow referencing the compromised tag.
At this time, we have no evidence that the payload was executed in any customer CI environment or that secrets were exfiltrated through the action.
C2 Infrastructure
The C2 server was hosted at Partner Hosting LTD (AS215826), registered at 71-75 Shelton Street, Covent Garden, London — a commonly used virtual office address. The infrastructure was freshly provisioned (subnet last modified just 5 days before the attack) and the IP was already associated with RATs, infostealers, and loaders in threat intelligence feeds. The infrastructure and tooling indicate a capable attacker familiar with CI/CD environments.
Exposure Assessment
Key Observations
Mutable tags are a known risk — but inertia is powerful
The GitHub Actions ecosystem has a well-documented problem: mutable tags. When users reference action@v5, they trust that the tag points to safe code. But tags can be force-pushed by anyone with write access. This is the #1 supply chain attack vector for GitHub Actions, and we knew it — yet our documentation still directed users to @v5.
Branch protection is not tag protection
Our branch protection rules worked exactly as designed. They prevented the malicious code from being merged into main. But the attacker didn’t need to merge — they just needed a commit in the repository (which any PR branch provides) and the ability to move a tag. Branch protection gave us a false sense of comprehensive security.
New features don’t protect retroactively
GitHub introduced release immutability in October 2025 – a feature that prevents tags associated with releases from being modified. We had this on our radar but had not fully understood its implications:
- It only protects tags associated with GitHub Releases, not standalone tags.
- It does not protect retroactively — existing releases created before enabling the feature remain mutable.
- Tag protection rules (a separate feature) must be configured independently.
Had we enabled release immutability and ensured the v5 tag was associated with a protected release, the tag poisoning would have failed.
Overly permissive GitHub App scopes
The GitHub App had write access that exceeded its operational needs. In a complex organization with multiple apps, bots, and integrations, it is easy for permissions to accumulate beyond what’s required. Each additional permission is an additional attack surface.
Corrections to the Public Record
The researchers’ report was instrumental in alerting the community, and we appreciate their rapid response. However, our internal investigation found some details that differ from their account:
- The timeline of the tag poisoning. The researcher’s report places the v5 tag move at approximately 10:49 UTC on March 3, immediately after the PRs were closed. Our investigation could not confirm this timing — tag force-push events are not recorded in GitHub’s repository activity log. What we know is that the tag was poisoned at some point after the malicious commit was created and before the community discovered it on March 9.
- The commit was not “signed with a maintainer’s email.” The researcher’s report describes the first malicious commit as “signed with a maintainer’s email address,” but this conflates git author metadata with cryptographic signing — these are fundamentally different things. The commit was not cryptographically signed. The attacker simply set the git author field to another maintainer’s email — something anyone can do, as git author metadata is self-reported and unauthenticated. The commit was pushed using the compromised maintainer PAT; the maintainer whose email was used was not compromised and only appears in the repository activity log starting at 12:21 UTC as part of the incident response team.
- The identities involved. The researcher described the maintainer identity and the GitHub App bot as evidence of “stolen credentials rather than insider action.” We can confirm that a maintainer’s classic PAT and the GitHub App’s private key were compromised. Both were used by the same external attacker. PR #48 appears under the ghost user because it was created by the GitHub App installation — not by a deleted human account.
- The number of affected repositories. The researcher’s report referenced 137+ repositories using @v5. Our review of GitHub code search results did not confirm that figure. At the time of our latest analysis, we found no public repositories actively using xygeni/xygeni-action@v5 in executable workflows. The references identified corresponded to documentation examples within Xygeni repositories, which have since been updated. In practice, most customers use the CLI-based scanner download and Xygeni’s Managed Scan feature, which internally invokes the action and uses a SHA-pinned, internally validated version that is not affected by tag manipulation. Since GitHub Code Search only indexes public repositories, we cannot determine with 100% certainty whether private repositories may have referenced the tag. Based on the available information, the actual downstream exposure appears to be significantly lower than initially reported.
[We will update this section as our investigation concludes.]
Response Actions
Immediate Response (March 3)
- Malicious PRs were flagged and blocked (branch protection prevented merge)
- Malicious code was extracted and preserved for forensic analysis
- C2 domain and IP recorded as Indicators of Compromise
- The compromised GitHub App (xygeni-onboarding-app-dev) was removed from the repository
- All contributor PATs were rotated
- Repository audit logs were reviewed — no evidence of prior unauthorized merges
Remediation Guidance
Remediation (March 9–10)
- The compromised v5 tag was removed
- Release immutability was enabled for the repository and enforced globally across all Xygeni-owned repositories
- Branch protection rules were hardened, including mandatory signed commits (Xygeni uses hardware-backed commit signing)
- The v5 tag was intentionally not re-created, to make clear that it had been compromised and to encourage migration to SHA-pinned references
- Documentation was updated to reference the full commit SHA (13c6ed2797df7d85749864e2cbcf09c893f43b23) corresponding to v6.4.0
- GitHub Actions were temporarily disabled on the repository as a precautionary measure
- Write permissions were restricted — only two designated maintainers and two repository administrators retain write access
For Users of xygeni-action
If you were using xygeni/xygeni-action@v5, you should:
- Immediately update your workflow to pin to the safe commit SHA:
uses: xygeni/xygeni-action@13c6ed2797df7d85749864e2cbcf09c893f43b23
- Audit your CI logs for any outbound connections to 91.214.78.178 or security-verify.91.214.78.178.nip.io during the period between March 3 and March 9, 2026.
- Rotate any secrets that were exposed to CI runners during that window.
- As an alternative, you may use a direct scanner download and verification
Why We Did Not Publicly Disclose on March 3
This is a question we owe an honest answer to.
On March 3, when our team responded to the malicious PRs, the assessment was that the attack had been fully contained at the PR stage. The branch protection rules had held. No malicious code had been merged into main. No CI runners had executed the payload. The compromised PAT was rotated, the GitHub App was removed, and repository audit logs showed no evidence of prior unauthorized merges. The incident was classified as Medium severity (P2) — a blocked intrusion attempt.
Based on this assessment, no public disclosure was deemed necessary. In hindsight, that assessment was incomplete.
What we missed was the tag poisoning. The v5 tag had been silently moved to point at the malicious commit, but this was not visible in the same audit surfaces we were reviewing. GitHub’s repository activity logs surface tag changes differently from branch operations, which made the modification less visible during the initial investigation. Our incident response was focused on the visible attack vector — the pull requests and the main branch — and did not check whether tags had been tampered with.
This is, in hindsight, one of the key lessons of this incident: you cannot disclose what you do not know about. Our response on March 3 was fast and effective against the threat we could see. But the attacker had a second, stealthier vector that went undetected for six days — until the community found it.
Public Disclosure (March 9)
On March 9, 2026, community members opened issue #54, questioning the malicious code and the compromised v5 tag. Researchers published a detailed analysis that helped raise awareness across the ecosystem.
We want to acknowledge the researcher’s role in amplifying the alert and providing actionable guidance to affected users. We also want to clarify certain details from their report where our internal investigation reached different conclusions — we address these in the Corrections section.
Lessons for the Ecosystem
- Pin actions by SHA, not by tag. Mutable tags are the single largest attack surface in the GitHub Actions ecosystem. Use action@<full-sha> in all production workflows.
- Understand the boundaries of each security feature. Branch protection protects branches. Tag protection protects tags. Release immutability protects releases. They are not interchangeable, and gaps between them are exactly where attackers operate.
- Audit GitHub App permissions relentlessly. Every installed app with write access is a potential lateral movement vector. Apply the principle of least privilege, rotate keys, and periodically review which apps are installed on critical repositories.
- Treat CI runners as hostile environments. Network egress monitoring, ephemeral runners, and secrets isolation are not optional for repositories in the software supply chain.
- New security features require proactive adoption. GitHub’s release immutability was available for months before this incident. Features that are not enabled provide no protection — security is not a default.
- Unsigned commits can have forged identities. Git commit author and committer fields are self-reported — anyone can set them to any value. Without cryptographic commit signing (GPG, SSH, or S/MIME), there is no guarantee that a commit was actually authored by the person it claims to be. In this incident, the attacker set the author of the first malicious commit to a different maintainer’s email, creating false attribution. Requiring signed commits via branch protection rules eliminates this vector.
- Complexity augments the attack surface. The interaction between PATs, GitHub Apps, branch protection rules, tag semantics, and release immutability created a landscape where the attacker found seams that none of these features individually covered. Simplify where you can. Understand the full threat model, even when you cannot.
Indicators of Compromise (IOCs)
| Type | Value |
|---|---|
| IP Address | 91.214.78.178 |
| C2 Domain | security-verify.91.214.78.178.nip.io |
| C2 Endpoints | /b/in (registration), /b/q (tasking), /b/r (exfiltration) |
| Auth Header | X-B: sL5x#9kR!vQ2$mN7 |
| ASN | AS215826 (Partner Hosting LTD) |
| Server | nginx/1.18.0 (Ubuntu) |
| TLS | Self-signed certificate |
Acknowledgments
We thank the security researchers and community members who reported this issue, including the contributors to issue #54 and the researchers for their public analysis. Transparency and collaboration are how we make the software supply chain more resilient for everyone.





