JFrog found that testmetest, a worm that steals credentials through npm packages, had embedded malicious code in 42 real third-party GitHub repositories. The repositories span 11 legitimate owners and include organization repositories.
Key points
- What happened: JFrog found that the testmetest npm worm had embedded roughly 13 MB of obfuscated code in 42 real GitHub repositories belonging to 11 owners.
- Why it went undetected: Attack files exceeded GitHub code search and Contents API indexing limits, making their very existence invisible to ordinary inspection methods.
- What makes it dangerous: Configuration files for Claude Code, Gemini CLI, Cursor, and VS Code launch attack code simply when a repository is opened. Destructive logic also attempts to delete the home directory when token revocation is detected.
- Inspection scope: Repository checks need to reach large Git blobs and development tools' automatic execution settings.
A 13 MB file outside the search index
According to JFrog's investigation, compromised repositories primarily contained a JavaScript file named .github/init.js. Recovered files were roughly 12.4–13.1 MB. GitHub code search does not index files this large, and the ordinary Contents API could not retrieve their contents either.
Even when configuration files contained suspicious references, the large target files could appear empty. Researchers traversed each repository's Git tree directly, retrieved the relevant Git blobs, and compared hashes. Obfuscated code recovered from 42 repositories fell into 10 wrapper variants, but all contained the same roughly 4.07-million-character loader, decoding to an identical 2 MB payload. File-size differences simply reflected the number of digits in character codes produced by nine ROT keys—1, 2, 3, 4, 14, 17, 18, 20, and 22. Decoded functionality was identical.
I confirmed that JFrog lists blob-group sizes ranging from 12,964,907 to 13,730,579 bytes. All 42 contained executable code; none was an empty placeholder.
Detection based only on filenames or surface strings can miss these variants. GitHub search and APIs are working normally. The attack exploits the search size limits themselves.
From packages to repositories: a credential-driven cycle
The attack has several components: malicious npm packages, execution mechanisms embedded in GitHub repositories, and concealed package publishing through GitHub Actions.
On npm, malicious programs launch during installation. Command expansion is embedded in the sources field of binding.gyp, while ordinary preinstall and postinstall entries remain empty. Because npm invokes the node-gyp C/C++ build system merely when binding.gyp exists, code runs even with nothing in the package's scripts section.
Stolen credentials enable writes to other repositories and publication of malicious packages. The attacker-controlled CementBeetleSteeple account had 17 repositories, with short-lived chore/ci-<hex> branches containing ci.js and Actions workflows. JFrog recovered 68 branch tips and 16 variants of obfuscated publishing code.
The decoded code checks whether the running workflow and repository match specified conditions, then collects credentials and examines permissions needed for npm Trusted Publishing. Specifically, it includes logic obtaining npm publishing authorization through GitHub Actions OIDC authentication, an OpenID Connect-based identity mechanism. Designed to authenticate package provenance, this mechanism can also become an attack foothold in a compromised environment.
Development tool settings become execution entry points
Repositories also contained configuration files associated with Claude Code, Gemini CLI, Cursor, and Visual Studio Code: .claude/settings.json, .gemini/settings.json, .cursor/mcp.json, .cursor/rules/setup.mdc, and .vscode/tasks.json.
These exploit development tools' automatic configuration loading when opening a repository folder. For Claude Code, a SessionStart hook specified running .github/init.js with node or bun. Tool-specific automatic execution settings therefore also belong within repository trust checks. Focusing only on compilation or npm installation misses this path.
Designed to evade detection
Package distribution has evolved as well. Initially observed packages launched JavaScript loaders through command expansion in binding.gyp. Some later packages omit the JavaScript body itself, instead combining a native Node.js addon, a compiled binary, with encrypted data disguised as a cache file (.napi_cache). The addon XOR-decodes the data with 0x96, writes a temporary file, and executes it.
Before execution, environment checks detect CI variables for GitHub Actions or GitLab CI, Docker or container traces, small virtual machines with no more than 1.5 GB of memory or one CPU core, and attached debuggers. Any match causes a silent exit. Windows versions additionally detect network and process analysis tools such as Wireshark and Process Monitor. Leaving no installation errors and remaining inactive in automated scanners' environments makes package scanning easier to evade.
JFrog describes the practical effect as execution on developer PCs and ordinary servers, but dormancy inside CI runners, containers, and sandboxes used by automated scanners.
Revoking stolen tokens can trigger destruction
On development endpoints suspected of infection, something must precede credential revocation. This attack family includes destructive logic checking stolen GitHub tokens every 45 seconds and attempting to delete the home directory after a 401 authentication error or 403 access denial. SafeDep's October 22 investigation reported this behavior, which continues monitoring for 48 hours.
The response order therefore requires first removing persistent malware, net-health-checker and pkg-runtime, then revoking credentials. Endpoint isolation, persistence discovery and removal, credential reissuance from a trusted environment, and repository and workflow audits must be combined.
A connection to cPanel scanning
The compromised accounts also contained a separate attack setup called "shellpanel." It uses GitHub Actions runners to scan and attempt exploitation of CVE-2026-41940, an authentication bypass in internet-facing cPanel web-hosting management panels and WHM. cPanel released fixes on April 28, and CISA added it to the Known Exploited Vulnerabilities (KEV) catalog on April 30 that year.
JFrog connected the scanning infrastructure reported by Socket in July 2026 with this testmetest repository intrusion activity. Successful compromise of the targeted cPanel servers has not been confirmed, providing no basis for adding them to this attack's victim count.
What is known—and what is not
Before JFrog's report, SafeDep published its npm-side analysis of testmetest on October 22. At that point, SafeDep had not confirmed compromised third-party packages or victims. By investigating GitHub infrastructure, JFrog reached different findings. It attributes the gap not simply to timing, but to payload sizes exceeding practical GitHub search and API limits, preventing discovery through ordinary methods.
The figure of 42 counts repositories containing malicious code. It does not establish execution in all 42 or credential theft from an equal number of users. An attacker-controlled repository accumulated encrypted data through a collection mechanism between October 22 and 24: 27 JSON files totaling roughly 375 KB. Their contents have not been decrypted, and victims have not been identified.
Code similarities suggest links to the Miasma family, an npm worm reported in June 2026, but attribution to the same people or group is unconfirmed. JFrog identified shared tooling and Shai-Hulud-family payloads without concluding that the original Miasma operators were responsible. Shai-Hulud source code has been public since May 2026, so other groups may be using it.
What is established is that attack code entered legitimate repositories and hid executable content beyond GitHub search visibility. Dependencies on npm, developers' GitHub credentials, CI/CD, and AI coding tools also coexist in Japanese development environments. For companies and projects, the issue is not merely whether they are among those 42 repositories, but whether their checks reach large Git blobs and development tool execution settings.
Originally published on Joho no Todai. The Japanese original is here.
Top comments (0)