BufferZoneCorp Go module or knot-* gem ever entered a build, treat that machine as compromisedThe Go payloads run from init() the moment a package is imported, and the gems run theirs during gem install or as soon as they are loaded. Rotate every SSH key, cloud credential, registry token and GitHub token the machine or runner could reach, remove the attacker keys from ~/.ssh/authorized_keys, and check .git/hooks/post-commit in every repository that was checked out on it.
Summary
- What: 17 packages from the GitHub account
BufferZoneCorp, created between April 20 and 24, 2026: ten Go modules and seven RubyGems published asknot-*. Socket discovered the campaign and published its analysis on May 1, 2026. The packages were listed in OSV on May 13 (MAL-2026-3620 to MAL-2026-3636), with Socket's post as the reference. - What we add: behaviour not in the earlier reporting. On the Go side: a fake
kubectlthat sends every command line out, apost-commitgit hook that reports each repository, the full stealer inlog-core's first commit, and a filter bug that makes the stealer take almost the whole environment. On the Ruby side: a second planted SSH key,RUBYOPTpersistence written into GitHub Actions by five gems, and a gem that installs the same git hook. We also found all 17 repositories still live on GitHub on October 2 and reported them. - What to do: search
go.mod,go.sum,Gemfile.lockand CI logs for the names below, hunt for the IOCs, and rotate secrets anywhere they ran.
What happened
On April 20, 2026 at 12:06 UTC a GitHub account called BufferZoneCorp was created. Between 12:28 and 13:26 UTC, about 80 minutes, it published fifteen repositories: Go libraries with ordinary names (go-retryablehttp, grpc-client, net-helper, go-metrics-sdk, log-core) and Ruby gems dressed as Rails tooling (activesupport-logger, devise-jwt-helper, rack-session-store, rails-assets-pipeline). config-loader followed on April 21 and go-stdlog on April 24. Each one has a tidy README and working library code. Most of them also carry a payload.
Socket found the campaign and published a detailed analysis on May 1, 2026, covering the credential theft, the SSH key, the Go proxy and logrus swap, the fake go binary and the HTTPS_PROXY redirect. On May 13 the packages were listed in OSV. Today proxy.golang.org returns no versions for any of the ten modules, and all seven knot-* gems are gone from rubygems.org.
The source code is a different story. When we checked on October 2, 2026, all seventeen repositories were still public on GitHub, with every payload intact. A Go module can be fetched directly from its repository with GOPROXY=direct or GOPRIVATE, and anyone can copy the code into a new module. We reported the account to GitHub the same day, and read every repository in full. This post describes the whole campaign and marks what we found that had not been reported before.
The registries took the packages down in May. The code that steals your keys stayed one git clone away.
Timeline
- Apr 20, 2026 12:06 UTCGitHub account BufferZoneCorp created.
- Apr 20, 12:28 to 13:26 UTCFifteen repositories created in about 80 minutes: the Go modules first, the Ruby gems last.
- Apr 20, 12:28 and 12:40 UTClog-core is published with a full credential stealer in its first commit. Twelve minutes later a commit titled "v0.2.0: remove telemetry" strips it out, leaving a clean decoy.
- Apr 21 to 23, 2026config-loader added; payload revisions pushed to the Go and Ruby repositories.
- Apr 24, 2026 08:21 UTCgo-stdlog, a CI and container reconnaissance module, added.
- May 1, 2026Socket publishes its analysis of the campaign.
- May 13, 2026 03:09 UTCAll 17 packages listed in OSV (MAL-2026-3620 to 3636), referencing Socket's analysis.
- May 14, 2026The ten Go modules enter CyberXYZ's malicious-package data; the CyberXYZ Go proxy, CLI and IDE extension block them from then on.
- Sep 29, 2026The seven
knot-*gems enter CyberXYZ's data through an OSV backfill. - Oct 2, 2026CyberXYZ confirms the modules are gone from
proxy.golang.organd the gems from rubygems.org, but all 17 source repositories are still live on GitHub. Reported to GitHub.
The packages
The Go modules split into roles. Some steal, some rewire the build, one does reconnaissance, and a few are clean decoys that make the account look like a real publisher.
| Package | Role | What it does |
|---|---|---|
| Go | go-stdlib-ext | Steals SSH keys, cloud, registry and Git credentials; plants an attacker SSH key; disables Go checksum verification and installs a fake go binary in GitHub Actions. |
| Go | grpc-client, net-helper | Same stealer; adds a post-commit git hook that reports the repository URL. grpc-client also installs a fake kubectl that sends every command line out. |
| Go | config-loader | Same stealer and git hook, with the collector left pointing at localhost:9999 unless overridden. |
| Go | go-retryablehttp | In GitHub Actions, routes all job traffic through an attacker proxy and installs a fake go that leaks GH_TOKEN. Named after HashiCorp's library. |
| Go | go-metrics-sdk, go-weather-sdk | In GitHub Actions, point GOPROXY at the attacker, turn off the checksum database and delete logrus from go.sum. |
| Go | go-stdlog | Reconnaissance: token presence, Docker socket, Kubernetes service account, container capabilities, cloud metadata reachability. |
| Go | log-core, go-envconfig | Clean at the latest commit. log-core carried the stealer in its first commit. |
| RubyGems | knot-activesupport-logger, knot-devise-jwt-helper, knot-rack-session-store, knot-rails-assets-pipeline, knot-rspec-formatter-json | Credential stealer with RUBYOPT persistence in GitHub Actions. Two run at install time from a native-extension build script, three when the gem is loaded. knot-rack-session-store plants an SSH key; knot-rails-assets-pipeline installs the git hook. |
| RubyGems | knot-date-utils-rb, knot-simple-formatter | Ship an empty native-extension build script and no payload; decoys that make the pattern look normal. |
How the main stealer works
go-stdlib-ext carries the most complete payload. Importing any package from it is enough; nothing has to be called.
A package-level init() starts a goroutine that waits 100 ms and then begins. There is no install hook in Go, so the payload runs in go test, go build code generation, and every binary that links the package.
File paths, the collector URL and the keyword list are byte slices XORed with a short key (sysutil1), and other names are split into fragments such as "GITHUB", "_ENV" and joined at runtime. A plain text search of the source finds none of them.
It reads the first 4 KB of ~/.ssh/id_rsa, ~/.ssh/id_ed25519, ~/.aws/credentials, ~/.npmrc, ~/.netrc, ~/.config/gh/hosts.yml, ~/.docker/config.json and ~/.kube/config, plus environment variables selected with a keyword filter (token, secret, pass, aws, github, credential and others). The filter is written with strings.ContainsAny, which matches any single letter of those words, so in practice it takes almost every variable in the environment.
The bundle, with hostname, username and a CI flag, is sent as JSON to a webhook[.]site URL. The variable PKG_ANALYTICS_URL overrides the destination, so the same build can be pointed at private infrastructure.
If the upload succeeds, it appends an ssh-ed25519 key with the comment deploy@buildserver to ~/.ssh/authorized_keys, creating the directory if needed. On a developer laptop or a self-hosted runner, that is persistent remote access.
Inside GitHub Actions it writes GONOSUMCHECK=* and GONOSUMDB=* to $GITHUB_ENV, drops a fake go script that reports its arguments and then runs the real toolchain, and adds that directory to the front of $GITHUB_PATH. Every later step in the job runs through it.
The CI attacks are the interesting part
Stealing files from a home directory is common. What sets this campaign apart is how much effort goes into GitHub Actions, where two files decide what every later step sees: $GITHUB_ENV sets environment variables for the rest of the job, and $GITHUB_PATH puts directories in front of PATH. A dependency that writes to them during one step controls the steps that follow.
Swapping a dependency through the Go proxy
go-metrics-sdk and go-weather-sdk write GOPROXY=<collector>|direct, GOSUMDB=off, GONOSUMDB=*, GOFLAGS=-mod=mod and a fresh GOMODCACHE to $GITHUB_ENV. Then they delete every github.com/sirupsen/logrus line from the project's go.sum. The effect: the next go command in the job has no recorded hash for logrus, no checksum database to ask, an empty module cache and an attacker-controlled proxy. It fetches logrus from the attacker. These two modules contain no stealer at all; their job is to make the build pull something else. The collector here is a webhook[.]site URL, which cannot serve modules, so as published this step would fail the build rather than swap anything. The mechanism is complete; only the server is missing.
The collector address is written as a list of what look like IP addresses, "104.116.116.112", "115.58.47.47", .... They are not addresses: each number is a character code, and the list decodes to the same webhook[.]site URL.
Tokens through a proxy header
go-retryablehttp takes a different route. It writes HTTPS_PROXY and HTTP_PROXY pointing at the collector into $GITHUB_ENV, so outbound traffic from later steps is directed there, and drops a fake go into the Go build cache that runs curl with X-Token: ${GH_TOKEN}, X-Run-Id and X-Repo sent as proxy headers before handing off to the real toolchain. The URL it requests belongs to a second module path, github[.]com/hashitools/go-retryablehttp, a HashiCorp lookalike that does not exist.
Git hooks and a fake kubectl
grpc-client, net-helper and config-loader walk up from the working directory to the nearest .git and prepend a line to .git/hooks/post-commit that sends the base64-encoded origin URL to the collector after every commit. The hook keeps working after the module is removed. grpc-client also places a fake kubectl first on the CI path that sends each command line, base64-encoded, before running the real one.
The Ruby side
Five of the seven knot-* gems carry a payload. Two run it at install time: they declare a native extension, and RubyGems runs the extension's ext/extconf.rb during gem install, before any application code loads. knot-activesupport-logger does this directly; knot-devise-jwt-helper keeps its version in a base64 blob handed to eval. The other three, knot-rack-session-store, knot-rails-assets-pipeline and knot-rspec-formatter-json, run when the gem is required. The two remaining gems ship an empty build script and no payload, so the native-extension pattern looks normal.
Each payload reads the same files as the Go stealer (SSH keys, cloud and registry credentials) and posts them with Net::HTTP. The default destination is hxxp://localhost:9999/collect; the real one comes from PKG_ANALYTICS_URL. Three things in the Ruby code had not been reported before:
- RUBYOPT persistence in CI. All five gems, when
$GITHUB_ENVexists, write a small Ruby file namedmonitor.rbinto a hidden directory in the home folder (~/.activesupport/lib,~/.devise-jwt/lib,~/.rack-session/bin,~/.rails-assets/lib,~/.rspec-fmt/lib) and appendRUBYOPT=-r<path>/monitor.rbto$GITHUB_ENV. From then on, every Ruby process in the job loads the attacker's file first, including test runs and deploy scripts that never touch the gem. - A second SSH key.
knot-rack-session-storeappends its ownssh-ed25519key, with the commentdeploy@rack-session, to~/.ssh/authorized_keys. Theknot-devise-jwt-helperblob also writes a key, but as published it decodes to code with corrupted names and a placeholder key (attackerPublicKeyHere), so that payload most likely fails. - The git hook, in Ruby.
knot-rails-assets-pipelineinstalls the samepost-commitbeacon as the Go modules, sending the base64-encodedoriginURL after every commit.
knot-rails-assets-pipeline only runs its stealer when RAILS_ENV looks like production or staging, when GITHUB_ACTIONS is set, or when PKG_TELEMETRY_ENABLED is set, which keeps it quiet on most developer laptops. knot-rspec-formatter-json records which CI system it is running in.
There is no install script in Go, nothing runs from go.mod, and every sensitive string in these modules is XORed, split into fragments or encoded as numbers. The code that touches $GITHUB_ENV only runs when that variable exists, so a scan on a laptop sees a library that does nothing. Static checks have to look for the shape of the behaviour, not the strings.
Indicators of compromise
| Type | Indicator | Context |
|---|---|---|
| Collector URL | hxxps://webhook[.]site/49c21843-c27c-4a1b-b1f6-037c3998055f | Default destination in the Go modules (XOR-encoded or written as fake IPs). Also listed in the OSV records. |
| Collector URL | hxxp://localhost:9999/collect | Default in the Ruby gems and config-loader; overridden by PKG_ANALYTICS_URL. |
| Environment variable | PKG_ANALYTICS_URL | Overrides the collector in every payload. |
| SSH key | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBp9VZGMxqFpTwKbKJi7dS2mNrX3LqEoHcYsWfAkZvUt deploy@buildserver | Appended to ~/.ssh/authorized_keys by go-stdlib-ext. |
| SSH key | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBcz8KqXJn2mP7dWvL9oRtYqNfEuHsAkGpMxZwQiVjTl deploy@rack-session | Appended to ~/.ssh/authorized_keys by knot-rack-session-store. New in this report. |
| CI environment | RUBYOPT=-r<home>/.{activesupport,devise-jwt,rails-assets,rspec-fmt}/lib/monitor.rb, .rack-session/bin/monitor.rb | Written to $GITHUB_ENV by five gems; loads the attacker file into every later Ruby process. New in this report. |
| File | ~/.config/{sysutil,grpcclient,netutil,cfgloader}/bin/ | Directories holding fake go or kubectl wrappers, added to $GITHUB_PATH. |
| File | $GOCACHE/go | Fake go dropped by go-retryablehttp. |
| File | .git/hooks/post-commit containing gc?r= | Beacon reporting the repository URL after each commit (three Go modules and knot-rails-assets-pipeline). New in this report. |
| File | $TMPDIR/diag-<hostname>.txt | Full environment dump written by go-stdlog outside CI. |
| CI environment | GONOSUMDB=*, GONOSUMCHECK=*, GOSUMDB=off, GOPROXY=<unknown>|direct | Written to $GITHUB_ENV by a dependency rather than by your workflow. |
| GitHub account | github[.]com/BufferZoneCorp | Created 2026-04-20; all 17 repositories. |
| Module path | github[.]com/hashitools/go-retryablehttp | Lookalike path requested by the fake go; does not exist. |
Search go.mod, go.sum and vendored code for BufferZoneCorp and hashitools, and Gemfile.lock for knot-. On developer machines and self-hosted runners, look for deploy@buildserver in ~/.ssh/authorized_keys, the ~/.config/*/bin directories above, and gc?r= in .git/hooks/post-commit. In GitHub Actions logs, look for GONOSUMDB, GOSUMDB=off or an unexpected GOPROXY or HTTPS_PROXY that your workflow did not set.
How CyberXYZ handles it
The ten Go modules have been in CyberXYZ's malicious-package data since May 14, 2026, and the CyberXYZ Go proxy, CLI and VS Code extension return decision: block for them. That covers installs that go through the CyberXYZ Go module proxy. It does not cover code fetched with GOPROXY=direct or copied into another module, which is why the live repositories matter. The seven gems were only added on September 29, 2026, through an OSV backfill, so CLI and SBOM scans flag them from that date. We do not run a RubyGems proxy, so nothing of ours would have stopped a gem install.
Writing this teardown also exposed a gap in our own pipeline, and we would rather say so. When we replayed go-retryablehttp through our Go content scanner, it scored zero: our checks looked for attack classes we had anticipated, such as standard-library impostors, suspicious replace directives and risky go:generate lines, and none of them describe what this code does. We have added checks for the behaviour seen here and in the earlier Go typosquat waves: writes to $GITHUB_ENV or $GITHUB_PATH from a package with an init(), XOR-decoded byte literals, a binary named go written next to a lookup of the real one, commands built from single-character strings, request-catcher endpoints, and init() spawning sh -c. The same module now scores 95 out of a promotion threshold of 80. Before shipping, we ran the checks over 2,691 real Go modules, including 207 of the most depended-on ones and 1,451 new modules from one week of the public Go module index. None reached our notification threshold.
Supply-chain risk analysis
Mapped to MITRE ATT&CK:
Recommended actions
Immediate. Remove any of the packages above. On every machine or self-hosted runner that built code importing them, rotate SSH keys, cloud credentials, npm and Docker tokens, gh tokens and Kubernetes credentials; delete the deploy@buildserver key from authorized_keys; clean .git/hooks/post-commit in affected repositories; and remove the ~/.config/*/bin wrapper directories.
Short-term. Treat a dependency that writes to $GITHUB_ENV or $GITHUB_PATH as hostile. Pin GOPROXY, GOSUMDB and GONOSUMDB explicitly in workflows so a later step cannot quietly change them, and give jobs the narrowest GITHUB_TOKEN permissions they need.
Long-term. A registry takedown removes the package, not the code. Route Go and Ruby installs through a checking proxy, and do not allow GOPROXY=direct fallbacks in CI, so that a known-malicious module is refused however it is requested.
References
- Socket, Malicious Ruby Gems and Go Modules Steal Secrets and Poison CI (May 1, 2026)
- OSV, MAL-2026-3622: Malicious code in github.com/BufferZoneCorp/go-metrics-sdk (Go)
- OSV, MAL-2026-3624: Malicious code in github.com/BufferZoneCorp/go-stdlib-ext (Go)
- OSV, MAL-2026-3632: Malicious code in knot-devise-jwt-helper (RubyGems)
- GitHub Docs, Security hardening for GitHub Actions
- MITRE ATT&CK, T1098.004 Account Manipulation: SSH Authorized Keys
- MITRE ATT&CK, T1195.001 Compromise Software Dependencies and Development Tools