Skip to content

Build1 publisher3 min readPublished

Go 1.27 executes a poisoned dependency at go test in a replay of npm's August 4 worm

Go 1.27 ran none of a poisoned module's code on go get, go build or go vet in a replay of npm's August 4 worm, but go test ran it with GITHUB_TOKEN in reach. Go teams still carry exposure through CI test runs, versions that stay cached for good, and bots that edit go.mod.

The Engineer · Build desk

Illustration accompanying Go 1.27 executes a poisoned dependency at go test in a replay of npm's August 4 worm
Generated illustration

What happened

  • Poisoned keyv and cacheable releases carried a preinstall script in package.json that npm runs automatically during npm install, before any package code is used.
  • The payload searched machines for cloud keys and GitHub, npm and CI tokens, then used the stolen npm tokens to republish poisoned versions of its victims' packages.
  • Socket flagged the first poisoned version six minutes after publication, too late for the first npm install runs, according to the post.
  • The Go team's stated design goal is that neither fetching nor building a module may execute its code, and Go has no preinstall equivalent.

Compiled by The EngineerSomething wrong?How this is made

Why it matters

  • exposure A CI job that runs go test or go run after a dependency bump hands that dependency's init code whatever tokens the job's environment holds.
  • decision Teams with update bots have to decide whether a bot-opened go.mod bump gets tested with secrets in scope before anyone reviews it, because the bot makes the go.mod change that Go's pinning waits for.
  • constraint A poisoned version stays in cache permanently, so a team's defence against a bad release has to act before go.mod points at it.

The Go side of the comparison starts with the module format. A module is an archive of sources and a hash, and nothing else [8]. The post's author replayed the worm's steps on Go 1.27 with a homemade local module whose init() function writes to stderr the moment it runs [9]. Real payloads are less talkative. Under go test ./..., it printed `[evil] init ran: HOME=/home/jules GITHUB_TOKEN present=true` [11]. According to the post, go run does the same, and the dependency runs with the caller's environment variables and tokens [12].

The author breaks a worm into three requirements: code that runs at install time, a token to publish with, and spread with no human touching anything [6]. With install hooks gone, test time is the remaining trigger. A CI job that runs go test on a dependency change gives that code the job's secrets [12].

Publishing is harder. A Go version is a tag in a git repository, and proxy.golang.org fetches that tag itself, so there is no registry account to take over [13]. A payload would need write access to the library's repository. The post concedes that a stolen GitHub token can provide it [14]. Its author argues that this access is more visible than a registry token in a config file, and is more often protected by review or signing [15].

The npm incident cuts against that argument. The attacker there did not break a signature. They pushed code into the repository, and the legitimate GitHub Actions pipeline signed the poisoned release with valid provenance [4]. Repository write access is exactly what a Go release needs [14]. The post cites Chainguard's point that provenance proves who published, not that the publisher's environment was trustworthy [16].

Spread is where Go's design holds. Under an npm range of ^6.0.0, a poisoned 6.0.1 arrives on the next npm install with no human decision [18]. Go moves no dependency version without a change to go.mod [19]. The post names update bots as a way in [21]. A bot that opens a go.mod bump and has CI test it makes that change and triggers go test in one step [12][19].

The checksum log has a second effect. sum.golang.org is a public log nobody can edit after the fact, so everyone receives the same bytes and an attacker cannot hand auditors a clean copy [17]. The post says Go also keeps a poisoned version in cache forever [20].

All of this rests on one author's replay with a test module on Go 1.27. The post does not describe an attack on Go modules in the wild. The author says the outputs shown are real, and the commands are short enough for any team to rerun against its own CI [9].

What to watch

  • A report of a poisoned Go module tag reaching proxy.golang.org through a stolen GitHub token, which would test the claim that repository access is better guarded than a registry token.
  • Changes to how Go dependency-update bots trigger CI test runs, the path the post names for a poisoned version to enter go.mod.
  • Any change from the Go team to the environment that dependency init code can read under go test or go run.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories