Safeguard
Supply Chain Attacks

The faker.js and colors.js Sabotage: What Maintainer-Driven Risk Teaches About Pinning

In January 2022 a trusted maintainer bricked two npm packages with a combined 26M+ weekly downloads — from his own account, with valid credentials.

Safeguard Research Team
Research
6 min read

On January 8, 2022, colors.js — a terminal-styling library pulled into roughly 19,000 downstream npm packages and averaging over 20 million downloads a week — started printing garbled, non-ASCII "Liberty Liberty Liberty" text in an infinite loop and hanging any Node.js process that imported it. A day earlier, on January 5, faker.js, a test-data generator with around 2,500 dependents and 2.4 million weekly downloads at the time, had been force-pushed with a version 6.6.6 that deleted the entire library, breaking every fresh install. Neither package was hacked. Both were sabotaged by their own maintainer, Marak Squires, who had published a GitHub issue back in November 2020 titled "No more free work," protesting unpaid corporate reliance on his open-source labor over a year before he acted on it. GitHub subsequently suspended his account, but by then the damage — thousands of broken CI pipelines — was already done. This incident remains one of the clearest public examples of a threat model distinct from credential theft or typosquatting: the legitimate, valid-signature maintainer as the attacker. This post breaks down what happened, why pinning worked for some teams and not others, and what actually would have caught it.

What did Marak Squires actually change in each package?

The two sabotages were mechanically different. In faker.js, Squires force-pushed a commit to the GitHub repository and published it to npm as version 6.6.6, gutting the package's source down to a near-empty file so that anything installing the latest version got a broken, non-functional library. In colors.js, the change was more disruptive at runtime: he committed an infinite loop — introduced under a commit adding what he called a "new American flag module" in the tagged release v1.4.44-liberty-2 — and backported it into the published npm versions 1.4.1 and 1.4.2. Any Node process that imported colors.js at that version hit the loop immediately on initialization, spamming the console with corrupted Unicode characters and hanging indefinitely, effectively a self-inflicted denial-of-service. Because both actions came from the actual maintainer's own credentials on the actual repository, there was no phishing, no stolen npm token, and no malicious pull request to review — the "attack" was a legitimate publish of a package everyone already trusted.

Why did pinning protect some projects and not others?

Semantic versioning saved projects that had pinned to an exact prior version. Faker.js's last stable release before the sabotage was 5.5.3; any project with a lockfile (package-lock.json or yarn.lock) resolving to 5.5.3, or a package.json using an exact version rather than a caret range like ^5.5.3, never pulled version 6.6.6 on a fresh npm install, because npm's semver resolution won't jump a caret range across major versions. Projects that specified a loose range spanning the sabotaged release, or that ran npm install without a committed lockfile in CI, pulled the broken version automatically the next time their pipeline ran. Colors.js is the more instructive counter-case: 1.4.1 and 1.4.2 were minor/patch bumps within the same major version as prior releases, meaning even a conservative caret range like ^1.4.0 would auto-resolve to the sabotaged version — pinning to a major version alone was not enough there, since the payload rode in on what looked like a routine patch release.

Could a package firewall have caught this before it installed?

Partially, and imperfectly, which is the honest answer. An install-time proxy that evaluates every fetch — the model Safeguard's Package Firewall uses in front of npm and pip — can run static, no-execution behavioral analysis on a package archive before it lands on disk, and it checks for known-malicious signatures and metadata anomalies as part of that pipeline. An infinite loop added directly to a well-known package's own source, published under the legitimate maintainer's own account with no new dependencies, no obfuscation, and no unusual install script, is a harder pattern for any static or reputation-based check to flag with confidence than a typosquat or a credential-harvesting post-install hook — this incident sits closer to a governance failure than a classic malware signature. What the firewall's audit trail does give a team is the forensic half of the answer: every non-allow decision is recorded per package, version, and ecosystem, so a team running in audit or warn mode would at minimum have a timestamped record of exactly when the sabotaged version entered their fetch path, even if it wasn't blocked outright.

What would have flagged the sabotage as it happened?

The strongest defensive angle here is behavior divergence from a package's own prior releases, not signature matching against known malware families. Safeguard's Eagle classification model scores artifacts across indicator classes that include exactly this kind of divergence — new post-install or runtime behavior, in this case an infinite loop introduced where none previously existed — and re-scores the historical corpus retroactively when its models improve, so that a package reclassified after the fact still generates a finding for anyone who installed it earlier. That retroactive-rescoring behavior matters specifically for a maintainer-driven sabotage: there is no way to know in advance that a trusted publisher will turn against their own package, so the value is in catching the divergence fast after publish and surfacing it to everyone who already has the version installed, rather than relying purely on a pre-install verdict.

What should teams actually change after an incident like this?

Sonatype and other researchers covering the incident converged on the same practical set of controls: pin exact versions in lockfiles and commit them to source control rather than relying on semver ranges to protect you, run installs through a caching or proxy registry (Snyk specifically recommended tools like Verdaccio) so your organization controls what versions actually enter your build rather than pulling straight from the public registry on every CI run, and override or pin transitive dependency versions explicitly in package.json when a dependency-of-a-dependency matters to your build's stability. None of these controls require distrusting every maintainer — they require treating "the maintainer published a new version" and "we're ready to consume that version" as two separate, deliberate steps instead of one automatic one.

How Safeguard helps close the gap

Safeguard's SCA engine resolves your full manifest-and-lockfile-aware dependency graph so a pinned version is verified against what's actually installed, not just what a manifest declares, and its malicious-package detection sits in the same unified findings model as CVE and license data rather than a separate tool you have to cross-reference. The Package Firewall adds the install-time control layer this incident argues for — audit mode lets a team baseline exactly what's entering their build before moving to warn or block — while Eagle's retroactive re-scoring is the piece purpose-built for the maintainer-turned-hostile scenario, since it watches for a package's own behavior changing release to release rather than only matching against known-bad signatures. What none of Safeguard's current tooling claims to solve is maintainer governance itself — there's no bus-factor or maintainer-health score in the platform today — so pairing technical controls with basic diligence on a dependency's maintenance health remains a team's own responsibility.

Never miss an update

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