In [Part 1] I pulled apart two compromises of one codebase by hand – a fake font that ran on folder-open, and a babel.config.js that turned every build into a remote-access trojan. Both, it turns out, belong to a publicly-documented campaign: public reporting attributes that family to a DPRK-linked actor tracked as PolinRider (also Void Dokkaebi). I didn’t prove that myself – my analysis was static – but the shared Tron dead-drop, the commit-spoofing tool, and the 166.88.54.158 C2 line up with what Trend Micro and others have published.
This post is the other half of the talk – the wider pattern those two incidents sit inside. And that pattern is an uncomfortable inversion of how we usually think about defense.
The thing that changed
For years the industry preached “shift left” – move security testing earlier, wire vulnerability scanners and code analyzers straight into the build pipeline, because catching problems early is safer. So we did. We handed a handful of automated tools the keys to everything: they pull private source, hold cloud deploy credentials, push to container registries, talk to Kubernetes.
The attackers watched us centralize all that access into a few tools, and made the obvious move: instead of attacking each application, they went after the tooling every application trusts.
GitGuardian’s 2026 State of Secrets Sprawl report counted 29 million secrets leaked to public GitHub in twelve months – API keys, cloud credentials, database passwords, tokens, in plaintext. A number that big resets your sense of the threat. The part worth sitting with, though, is how those secrets get taken – increasingly, by the security tooling itself.
Think about the math from the attacker’s side. A poisoned JavaScript date library only reaches the apps that import it. A security scanner or CI action is a different prize: by definition it touches everything you do, and it holds the credentials to pull private source, deploy to the cloud, and push to registries. Compromise one of those and you inherit that reach across every project that runs it – orders of magnitude more than any single library could give you. That’s why build tools and scanners became the target.
Patience: XZ Utils and “Jia Tan” (March 2024)
If you want to understand the mindset, start with the XZ Utils attack – the “Jia Tan” operation. It rewrote the playbook the slow way: two years of patience instead of a clever exploit.
The Jia Tan persona spent 24 months as a genuinely helpful contributor to the XZ compression library: fixing real bugs, improving performance, building social capital with the maintainer. Sock-puppet accounts showed up to complain the project was moving too slowly and to pressure the maintainer into handing over commit access “to ease the burden.” They manufactured a crisis and volunteered as the fix.
Once they had the keys, they hid the backdoor somewhere nobody reviews: binary test files – the opaque blobs projects keep around to test how code handles corrupt input – and triggered it during the build using tr, the ancient Unix character-translation utility. tr runs in millions of benign build scripts, so it trips no scanner. A substitution cipher over a test file, pulled apart and executed at build time, flagged by nothing.
That’s the baseline now: an actor patient enough to spend years earning trust before spending it.
Reaching into memory: tj-actions (March 2025)
The tj-actions compromise is a master class in abusing transitive trust. The target, tj-actions/changed-files, is used in over 23,000 repositories and does something mundane: on a pull request, work out which files changed.
The attackers didn’t hit it directly. They went upstream to the reviewdog org and exploited GitHub’s automated team-invitation flow – submit enough approved PRs (typo fixes, doc tweaks) and some configs will auto-invite you onto the team with write access. The machine handed over the keys. From there a dependency chain did the rest: the 23,000 downstream repos never changed a line of their own code. They ran their normal CI one morning and the malicious code executed on their runners, inheriting every privilege tj-actions had.
Here’s the part I keep coming back to. The payload didn’t just grep the filesystem for hardcoded passwords – it reached into the runner’s active process memory. On Linux everything is a file, memory included: under /proc/<pid>/mem is a direct window into a running process’s RAM. The code found the GitHub Actions runner’s PID, opened its mem, and scraped live, hunting the internal secret structure ({"name": ..., "value": ..., "isSecret": true}).
That walks straight around GitHub’s secret masking. Masking is a log-rendering filter – right before a line is printed, known secrets get swapped for asterisks. Reading memory intercepts the secret long before it reaches the renderer. Masking guards the log; the attacker took the secret one step upstream, out of RAM. You were defending the wrong layer.
Mutable tags: Trivy (March 2026)
Trivy – one of the most widely-used open-source vulnerability scanners there is – escalated the pattern across three waves, and each wave is its own lesson.
Wave one: the wrong trigger. A bot exploited Trivy’s use of the pull_request_target GitHub Actions trigger. The distinction that bit them:
pull_requestruns a fork’s untrusted code in a restricted, read-only context with no secrets. Safe.pull_request_targetruns in the context of the base repo – full write scope, all repo secrets – for when you genuinely need secrets during a PR build. Misconfigure it and you’ve handed those secrets to untrusted code.
Trivy had it configured without adequate guards, and the bot used that to exfiltrate a highly-privileged personal access token.
Wave two: incomplete rotation. It was caught, disclosed, and credentials were rotated – the human tokens. The bot tokens were missed. This is a near-universal blind spot: human tokens live in secret managers with 90-day rotation and alarms; automated service-account tokens get created once during setup, buried in a repo setting, and forgotten. Because one bot token survived the purge, the attacker kept silent access for roughly 18 days, holding a master key, waiting for attention to fade.
Wave three: tag poisoning. With the surviving bot token, the attacker force-pushed 76 of 77 version tags to point at a new malicious commit. This breaks an assumption everyone makes: that a tag like v0.24.0 is a snapshot carved in stone. Git tags are lightweight, mutable pointers – the attacker just moved v0.24.0 from the real commit to theirs. The poisoned entry-point script went from 2,855 bytes to 17,592 – roughly 6x – and nothing flagged it, because they spoofed the commit timestamps (Git trusts whatever date your machine reports, same trick as the temp_auto_push.bat from Part 1). Any responder building a timeline from the GitHub web UI was reading forged evidence.
It got faster, and it learned to sign itself
I could keep going – the talk did – but two developments matter most.
First, speed. GitGuardian documented a 48-hour stretch in April 2026 where three separate campaigns hit npm, PyPI, and Docker Hub at once. One worm, hidden in an npm post-install hook, jumped ecosystems: fire on install, scan the machine, and if it found an npm publish token it infected the victim’s npm packages – if it also found a PyPI token in a .pypirc, it crossed from Node into the developer’s Python projects. The worm is language-agnostic by design: hold both an npm and a PyPI token, and it walks from one ecosystem straight into the other.
Second, and worse: attackers learned to hijack provenance instead of faking it. SLSA Build Level 3 provenance is supposed to be the gold standard – a cryptographic signature proving a package was built by a specific trusted pipeline and not tampered with. In the mini Shai-Hulud attack on the TanStack ecosystem, the operators went through SLSA rather than around it. They stole the pipeline’s ambient OIDC token from the runner’s memory, authenticated to npm as the legitimate pipeline’s identity, and published malware. npm’s signing infrastructure verified the token and generated a perfectly valid SLSA L3 attestation for the malware.
And that’s the trap in it. SLSA L3 tells you which pipeline built an artifact. Whether that pipeline’s environment was clean, or acting on hostile instructions, is a question it was never designed to answer. The attestation was mathematically valid; the thing it vouched for was malware.
(And the blockchain C2 from Part 1 shows up here too – the newer campaigns moved C2 onto the Internet Computer Protocol, serving from icp0.io domains. No registrar, no host, no abuse@ to email. The unkillable dead drop, same reasoning as the Tron/Aptos pointer chain I walked through last time.)
The layer we keep forgetting: config and humans
Not all of this is exotic. In March 2026 a source map was accidentally published to public npm alongside minified code – a source map is the JSON “decoder ring” that maps minified JavaScript back to readable source. Publishing it leaked something like 500,000 lines of proprietary source. Human error, not a breach. But within hours, attackers read the recovered source, found the internal private package names it referenced, and registered those exact names on public npm. Developers pulling the leaked code to build it locally ran npm install, the resolver couldn’t find those deps in the private registry it didn’t have access to, fell back to public npm, found the squatted malicious packages with precisely the right names, and installed them. A missing exclusion rule in a build config became a targeted attack in under a day.
That’s the recurring truth under all of this: the config layer is the real control surface. A typo in a build config is as dangerous as a memory-corruption bug in your C++.
What actually helps
The frameworks are behind – CloudSec points out MITRE ATT&CK still has no mapping for abusing CI/CD trigger scopes or for blockchain-hosted C2. So stop waiting on the map and fix the infrastructure. Four things, in rough order of leverage:
- Pin to commit SHAs, not tags. A tag is a mutable label; a SHA is an immutable fingerprint of exact bytes. If an attacker swaps the code, a SHA-pinned build simply breaks instead of running the swap. A broken build costs you an afternoon; a credential-stealing worm costs you everything the credential can reach. This one change would have stopped the Trivy tag-poisoning cold.
- Audit bot and service-account tokens first – not last. They’re the ones without a human to notice, without a default expiry, and the first thing skipped in a panicked rotation. Give them the same 90-day discipline as privileged human accounts, and in an incident rotate the headless tokens before the human ones. That single omission gave Trivy’s attacker 18 extra days.
- Contain the dev environment. Endpoint antivirus loses to memory-resident, hook-bypassing worms. A cloud development environment – a containerized workspace with gated egress – moves the security boundary off the laptop. If a poisoned package (or an AI agent acting on your behalf) runs, it can’t pivot to the VPN or exfiltrate past a deny-by-default egress rule.
- Watch the size of your pipeline entry points. It sounds too dumb to matter, but Trivy’s wrapper ballooned 6x. Ten lines of bash that hash the downloaded action and compare its byte size to a known baseline – halt and page a human if it jumps – would have caught it before a single secret left memory.
Wrapping Up
The throughline from Part 1 to here is one idea: the target moved from the code to the trust. The fake font and the babel RAT worked because opening a project already runs code – the thesis of Part 1. XZ, tj-actions, and Trivy worked because we’d handed our trust to a few tools and never checked whether that trust could be forged, inherited, or quietly moved to a different commit.
And the trajectory points somewhere I find genuinely unsettling. We spent this whole series on attackers tricking human developers and exploiting pipeline config. But autonomous AI coding agents now review code, open PRs, manage dependencies, and run shell commands on our behalf. So the question I left the talk audience with, and I’ll leave you with: what happens when the next “Jia Tan” is an adversarial prompt hidden in a README instead of a person patiently earning commit access – one written to convince your highly-privileged AI agent that a backdoored dependency is the safest, most optimal choice for the thing it’s building for you? We’ve spent three years learning to defend the code and the pipeline. The next surface to defend might be the agent’s judgment.
Check your logs, rotate your bot tokens, and audit your trigger configs – this week, not next quarter.
Disclaimer: This is a defensive, educational write-up. The specifics above are drawn from published threat-intelligence reporting (GitGuardian, ThreatLocker, StepSecurity, CloudSec, Unit 42, Aqua Security, Coder) and from a talk I gave; where a detail rests on a single low-confidence source I’ve kept it general. Attribution of any named campaign is cited to that public reporting, not claimed as my own finding.