```html ```
top of page

A Seven-Year-Old RubyGem Is Trusted By Design. SleeperGem Turned That Into the Attack, and Our Own Scanner Said Allow.

  • Writer: Patrick Duggan
    Patrick Duggan
  • 2 days ago
  • 7 min read

Three malicious gems went up on RubyGems this weekend. Two of them came from real maintainer accounts that had been sitting quietly since 2019 and 2020. That dormancy was not incidental to the attack. It was the attack.


We publish a package-reputation scanner. I ran it against those gems this morning. It said allow.


Here is what happened, why our detector was wrong, and what it does now.



What SleeperGem is


Researchers at StepSecurity and Aikido Security disclosed a Ruby-ecosystem supply-chain campaign they named SleeperGem. Three packages:


git_credential_manager, versions 2.8.0 through 2.8.3, published July 18. This one is straightforward attacker work — a fabricated gem impersonating Microsoft's Git Credential Manager. It never had a legitimate life.


Dendreo, versions 1.1.3 and 1.1.4, published July 18. This one is not fabricated. It is a real gem whose last honest release, 1.1.2, shipped on October 24, 2020. The maintainer account, LR-DEV, went quiet for roughly six years and then published malware.


fastlane-plugin-run_tests_firebase_testlab, version 0.3.2, published July 19. Same shape. Last legitimate release March 4, 2019. The account, pinkroom, was dormant about seven years.


Aikido's Charlie Eriksen put the thesis plainly: a RubyGems account that has gone quiet for six or seven years doesn't look risky to anyone, and that is exactly the profile worth taking over. Not a long-game planted asset — a real, ordinary, abandoned account that looked harmless enough to hijack without anyone noticing.


The payload confirms the targeting. The gems fetch a second stage from a Forgejo host, then scan the machine for roughly thirty environment variables belonging to GitHub Actions, GitLab, CircleCI, Travis, Jenkins, and Vercel. If it finds any of them, it exits. It does not want your build runner. Ephemeral CI is a bad host: it dies in minutes and it has nothing personal on it. The malware is checking whether it landed on a human's laptop, and only continuing if it did. What it installs there is a daemon under the user's local share directory, persistence via both a systemd user service and a cron entry, and a setuid-root shell dropped at a path chosen to look like a networking utility.



Why our scanner said allow


We built a package-reputation primitive last week for a different campaign, one where a poisoned dependency hid inside an innocuous proof-of-concept repo. The design principle was deliberately name-independent: don't ask what a package is called, ask how it behaves. Is it brand new? Does anyone actually download it? Has it been removed from the registry?


That primitive scores a package on three behavioral facts, and one of them was doing damage. Age was a trust signal. A package under thirty days old added risk. A package over two years old with real download volume short-circuited to a risk score of zero and was declared trusted, full stop, regardless of anything else.


Read that against SleeperGem and the failure is obvious in hindsight. Dendreo is nine years old. The fastlane plugin is eight. Under our scoring, age was pedigree, and pedigree was precisely what the attacker stole. We had built a detector that rewarded exactly the quality the campaign was harvesting.


There were two more problems underneath that one, and they were worse because they were quiet.


Our scorer supported PyPI and npm. It routed any other ecosystem to PyPI by default. So asking it about a RubyGem sent the query to the wrong registry, got a 404 because the gem obviously isn't a Python package, and returned the confident-sounding verdict not-on-registry, removed or never-existed. A plausible answer assembled entirely from false evidence. That is worse than an error, because an error tells you to look again.


And our public check-package tool, the one an AI coding agent calls before running an install, accepts only npm and pypi as ecosystems. A developer's agent about to install a Ruby gem could not ask us the question at all.



What it does now


Age is no longer a trust signal on its own. The scorer now reads the whole publish timeline rather than just the first release, and computes the gap between the most recent release and the one before it. Long silence followed by a sudden release is the hijacked-maintainer shape, and it now carries substantial risk. Crucially, the established-and-trusted short-circuit no longer applies to a revived package — pedigree cannot cancel the revival flag, because pedigree is the thing being borrowed.


The distinction that matters: an old package is not a trusted package. An old package that has been continuously maintained is. Dormancy by itself is not an attack either — a gem that has been quiet for six years and is still quiet is just finished software. It is the reawakening that carries signal.


RubyGems is now a first-class ecosystem. And unsupported ecosystems now decline outright, returning an explicit unknown, with a signal that says so in words: no registry queried, this is not a safety judgement. Declining to answer is a real answer. Guessing wearing a confident face is not.



The part that limits the claim


I want to be precise about what we can and cannot demonstrate, because the honest version is less impressive than the version I'd rather write.


RubyGems yanked the malicious versions within about two days of disclosure. Query the API right now and Dendreo's newest version is 1.1.2 from 2020, and the fastlane plugin's is 0.3.1 from 2019. The registry has been restored to its pre-attack state. Which means the revival signal, run live today, sees nothing to fire on — and correctly stays quiet.


So a live test against those packages proves nothing. It would pass by accident. We wrote the regression test against the real live-window timeline instead — the publish dates as they stood on July 19, which is the state a developer's machine actually saw at install time. Eight cases: both hijacked gems trip the revival flag; a heavily-downloaded old package with a revival still refuses to be declared trusted; a continuously-maintained popular package stays clean; a still-dormant package does not false-positive; a single-release package doesn't crash the gap arithmetic; the unsupported-ecosystem guard declines. Eight pass.


That is the honest scope: this detector works during the live window, which is the window that matters, and goes quiet after takedown, which is when it no longer does. The window is where developers get hit.



What we found in our own plumbing on the way


Covering a campaign isn't finished until its indicators are in the feed where someone can actually pull and block them. Going to do that, we hit our own wall.


Our verified ingest endpoint rejected all three gems with invalid-type: package. The endpoint's type allowlist has six entries — hash, IP, domain, URL, CVE, hostname — and package is not among them. Meanwhile our indicator index holds over two hundred thousand package records and we publish a package deny-list built from them. Those arrive from the automated OSV syncs, which write to the database directly and never touch the endpoint. So the one indicator type our entire supply-chain product rests on could not be written through our own curated ingest path, and nobody noticed, because the automated lane that carries 99% of the volume goes around it.


That is fixed in code, along with a related quiet bug: a hand-written batch that supplied its citations as a string rather than an array had them silently discarded, dropping the evidence that justifies the record. Both are pending deploy. The campaign's second-stage host is in the feed now, verified. The three package indicators land when that ships, and I'd rather say that plainly than imply a completeness we don't have yet.


One scoping note on that host, because it matters and it is easy to get wrong. The second stage is served from a Forgejo instance run by a legitimate privacy collective. The malicious thing is one account path impersonating an official Git ecosystem organisation. We indexed the path. We did not blocklist the domain, and neither should you — that would cut off a real service used by real people to punish it for hosting an abuser. Blocking the building because of one tenant is how threat feeds create victims.



What to actually do


Check whether any of the three named versions are in your lockfiles: git_credential_manager 2.8.0 through 2.8.3, Dendreo 1.1.3 or 1.1.4, fastlane-plugin-run_tests_firebase_testlab 0.3.2. The clean releases below those are still clean; this is not a reason to abandon the packages.


If you find one, assume the developer machine and not the pipeline, because that is what the malware was choosing. Look for a daemon under the local share directory, a systemd user service and a cron entry you didn't create, and a setuid-root binary sitting somewhere in the system path that has no business being setuid.


And the general lesson, which is the one worth keeping after the specific gems are forgotten: audit your supply-chain rules for anywhere age, popularity, or longevity are being used as a proxy for trust. Those are reputation signals, and reputation is transferable. An attacker who takes over a dormant account inherits every point of trust that account accumulated, and inherits it instantly. The question a scanner should ask is not how long has this existed but has this been continuously cared for, and does the pattern of care look unbroken.


We had that wrong for a week. We were two days late catching it, and the researchers who found it did the real work. That is the accurate version.




Primary research and disclosure: StepSecurity and Aikido Security (Charlie Eriksen) — StepSecurity's SleeperGem writeup and The Hacker News coverage. The detection gap, the fix, the regression suite, and the ingest bug are ours. Package details and dormancy dates as reported by those sources and verified against the live RubyGems API on 2026-07-20.




Every indicator in this post is in the feed. Free.

1.58M+ IOCs, STIX 2.1 / TAXII, 88% novel vs ThreatFox, exploited-CVE leads ahead of CISA. No credit card — a free API key in 30 seconds, and you can audit every claim above against the live endpoints.


Comments

Rated 0 out of 5 stars.
No ratings yet

Add a rating
bottom of page