<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: quietkernel</title>
    <description>The latest articles on DEV Community by quietkernel (@quietkernel).</description>
    <link>https://dev.to/quietkernel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4201116%2Fc465dc21-9b54-479d-a69a-3d5067df50de.png</url>
      <title>DEV Community: quietkernel</title>
      <link>https://dev.to/quietkernel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/quietkernel"/>
    <language>en</language>
    <item>
      <title>How npm worms steal your credentials, and the order to rotate them in</title>
      <dc:creator>quietkernel</dc:creator>
      <pubDate>Thu, 22 Oct 2026 19:31:57 +0000</pubDate>
      <link>https://dev.to/quietkernel/how-npm-worms-steal-your-credentials-and-the-order-to-rotate-them-in-5fd1</link>
      <guid>https://dev.to/quietkernel/how-npm-worms-steal-your-credentials-and-the-order-to-rotate-them-in-5fd1</guid>
      <description>&lt;p&gt;Almost every npm supply-chain advisory ends with the same instruction: rotate every credential the machine could access. It sounds simple until you try it at 2 AM. Which credentials did the machine have? Where are they stored? And does the order matter?&lt;/p&gt;

&lt;p&gt;The order does matter. In at least one recent case, doing the obvious thing first deletes your home directory.&lt;/p&gt;

&lt;p&gt;This post walks through what a malicious package can read on a typical developer machine, why worms in particular go after some credentials before others, and a rotation order that holds up. The worked example at the end uses a small open-source CLI, but the order applies whether you use a tool or a checklist on paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a malicious package gets to run
&lt;/h2&gt;

&lt;p&gt;npm packages can declare lifecycle scripts in &lt;code&gt;package.json&lt;/code&gt;: &lt;code&gt;preinstall&lt;/code&gt;, &lt;code&gt;install&lt;/code&gt; and &lt;code&gt;postinstall&lt;/code&gt;. When you run &lt;code&gt;npm install&lt;/code&gt;, npm runs those scripts with your user's permissions. No prompt, no sandbox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"innocent-looking-lib"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.2.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"preinstall"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node ./setup.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever is in &lt;code&gt;setup.js&lt;/code&gt; can read any file your user can read and make network requests. That is the whole attack. Two real examples from this month show what the payloads do in practice.&lt;/p&gt;

&lt;p&gt;On October 8, 2026, &lt;code&gt;tensorlake@0.5.144&lt;/code&gt; was published from a compromised maintainer account. According to &lt;a href="https://github.com/tensorlakeai/tensorlake/security/advisories/GHSA-8g63-53c9-65j2" rel="noopener noreferrer"&gt;the project's own advisory (GHSA-8g63-53c9-65j2)&lt;/a&gt;, the payload ran from a &lt;code&gt;preinstall&lt;/code&gt; hook and harvested "GitHub and npm tokens, cloud provider credentials (AWS, GCP), Kubernetes and Vault tokens, SSH and GPG keys, .env files, browser-saved passwords, cryptocurrency wallets, and AI tool and MCP configuration." It then used the stolen tokens to republish itself into other packages the victim could write to. The version was live for about an hour and forty minutes, and it shipped with valid npm provenance because the attacker published through the project's own release workflow.&lt;/p&gt;

&lt;p&gt;A few days earlier, &lt;code&gt;@subql/common@5.8.3&lt;/code&gt; shipped a &lt;code&gt;postinstall&lt;/code&gt; hook that decoded and ran an obfuscated payload. The &lt;a href="https://osv.dev/vulnerability/MAL-2026-17571" rel="noopener noreferrer"&gt;OSV entry MAL-2026-17571&lt;/a&gt; has the analysis.&lt;/p&gt;

&lt;p&gt;Neither of these is new as a technique. CISA's &lt;a href="https://www.cisa.gov/news-events/alerts/2025/09/23/widespread-supply-chain-compromise-impacting-npm-ecosystem" rel="noopener noreferrer"&gt;September 2025 alert on the Shai-Hulud worm&lt;/a&gt; describes the same pattern: steal tokens, then use them to publish infected versions of the victim's own packages.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a stealer reads first
&lt;/h2&gt;

&lt;p&gt;Most of these payloads don't need anything clever. Developer credentials sit in well-known plaintext files:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What's in it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.npmrc&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;//registry.npmjs.org/:_authToken=...&lt;/code&gt;, your publish token&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;~/.config/gh/hosts.yml&lt;/code&gt;, &lt;code&gt;~/.git-credentials&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;GitHub tokens, when no OS keyring is in use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.aws/credentials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AWS access keys, often long-lived&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.kube/config&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;cluster tokens and client keys&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;~/.ssh/id_*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;private keys, sometimes without a passphrase&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;.env&lt;/code&gt; files in your projects&lt;/td&gt;
&lt;td&gt;database URLs, payment keys, webhook secrets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MCP configs (&lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;, Claude Desktop config, etc.)&lt;/td&gt;
&lt;td&gt;API keys pasted directly into &lt;code&gt;env&lt;/code&gt; blocks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;process.env&lt;/code&gt; in CI&lt;/td&gt;
&lt;td&gt;whatever secrets the job passes to the install step&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can check your own exposure for npm in one line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'_authToken|_auth|_password'&lt;/span&gt; ~/.npmrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that prints a literal token (not &lt;code&gt;${NPM_TOKEN}&lt;/code&gt;), any install script on this machine can read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the order matters
&lt;/h2&gt;

&lt;p&gt;When a machine is compromised, you're racing two things: the malware that may still be running, and the stolen credentials that are already being used elsewhere. Rotating in the wrong order can let the attacker re-steal what you just rotated, or set off something worse.&lt;/p&gt;

&lt;p&gt;Here is the reasoning, step by step.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Stop the malware first.&lt;/strong&gt; If the payload installed something persistent, every credential you rotate from that machine gets stolen again. The tensorlake payload made this concrete. Its advisory says it installs a service named &lt;code&gt;gh-token-monitor&lt;/code&gt; "that deletes the user's home directory if the stolen GitHub token is revoked," and tells you to remove it before rotating any GitHub token. So the first step is to disconnect the machine, remove persistence, and do every rotation after this from a different, clean machine.&lt;/p&gt;

&lt;p&gt;On Linux the advisory's removal steps are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; disable &lt;span class="nt"&gt;--now&lt;/span&gt; gh-token-monitor.service
&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; ~/.config/gh-token-monitor ~/.local/bin/gh-token-monitor.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl bootout gui/&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
&lt;span class="nb"&gt;rm&lt;/span&gt; ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also check &lt;code&gt;crontab -l&lt;/code&gt;, systemd user units, LaunchAgents and &lt;code&gt;.vscode/tasks.json&lt;/code&gt; for anything you didn't add.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Package registry tokens next.&lt;/strong&gt; This is what makes a worm a worm. A stolen npm token lets the attacker publish an infected version of every package you maintain, which then runs on every machine that installs it. Every minute that token is live, the infection can spread past you. Revoke it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm token list
npm token delete &amp;lt;token-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;npm's own docs note that &lt;a href="https://docs.npmjs.com/revoking-access-tokens" rel="noopener noreferrer"&gt;revocation can take up to an hour&lt;/a&gt; to take effect. Then run &lt;code&gt;npm view &amp;lt;your-package&amp;gt; time&lt;/code&gt; for each package you maintain and look for versions you didn't publish. The same logic covers PyPI, RubyGems and crates.io tokens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. GitHub.&lt;/strong&gt; GitHub tokens let the attacker push commits, add workflows and read private repos. They come after npm because they're slightly less direct as a spreading channel, and they come after step 1 because of the hostage-service trick above. Revoke tokens at &lt;code&gt;github.com/settings/tokens&lt;/code&gt; and OAuth apps at &lt;code&gt;/settings/applications&lt;/code&gt;, then &lt;code&gt;gh auth logout&lt;/code&gt; and &lt;code&gt;gh auth login&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Cloud and infrastructure.&lt;/strong&gt; AWS, GCP, Azure, Terraform, Kubernetes. These are often the most damaging credentials, but they don't spread the worm to other people, so they come after the spreading channels. For AWS, create a new key before you delete the old one so you don't break running systems, and check CloudTrail for reads of SSM Parameter Store and Secrets Manager during the exposure window.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Deploy platforms and CI.&lt;/strong&gt; Anything stored in a platform's env store, plus the secrets your CI passes to steps. Remember to redeploy, because most services read environment variables at startup and keep the old values until they restart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. AI provider keys, SSH keys, and application secrets.&lt;/strong&gt; Still important, still need doing, but they don't let the attack spread.&lt;/p&gt;

&lt;p&gt;So the order is: stop the code that's running, close the channels the attack uses to spread, then clean up everything else, most damaging first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Worked example: encoding the order in a tool
&lt;/h2&gt;

&lt;p&gt;I wrote this order into a small CLI called &lt;a href="https://github.com/Krrishkumar125/secret-blast-radius" rel="noopener noreferrer"&gt;secret-blast-radius&lt;/a&gt;, so a scan can turn "rotate everything" into a concrete list. The interesting part is how little code the ordering needs. Each finding the scanners produce has a &lt;code&gt;category&lt;/code&gt;, and the categories map to a table in &lt;code&gt;src/index.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ROTATION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;persistence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Stop the malware first (worm services, malicious packages/extensions)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* steps, verify, docs */&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;npm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Package registry tokens: npm, PyPI, RubyGems, crates.io, Composer (stops a worm republishing your packages)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GitHub tokens and sessions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;cloud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cloud and IaC credentials (AWS, GCP, Azure, Terraform, Pulumi)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;kubernetes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Kubernetes and container registries&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;paas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Deploy platforms (Railway, Vercel, Fly, Render, Heroku, Netlify)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;redeploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Secrets present in the environment / CI&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;redeploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AI provider keys and MCP servers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;ssh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SSH keys&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;order&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Application secrets in .env files&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;redeploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Trimmed. Each entry also carries &lt;code&gt;steps&lt;/code&gt;, a &lt;code&gt;verify&lt;/code&gt; check that the old credential is dead, and links to the provider's docs.)&lt;/p&gt;

&lt;p&gt;The runbook is then a filter and a sort. It keeps only the categories where the scan actually found something, so you don't get a 12-step plan for a machine that only has an npm token and an SSH key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runbook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ROTATION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ROTATION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ROTATION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;cats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;ROTATION&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;findings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;category&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Findings in the normal report are sorted by severity first and rotation order second, so two critical findings show up with the persistence one above the npm token.&lt;/p&gt;

&lt;p&gt;If you write your own tooling, one choice I'd copy is that findings meaning "you're already infected" go into the &lt;code&gt;persistence&lt;/code&gt; category, not their own. A known-bad version in a lockfile, a worm marker dependency, or a VS Code task that runs a disguised file all land in step 0. If you've installed a malicious version, the first job is cleaning the machine, not rotating keys from it.&lt;/p&gt;

&lt;p&gt;The scanner checks the plaintext locations from the table above and prints a short fingerprint (the first 8 hex characters of a SHA-256) instead of the value, so you can tell two tokens apart without the tool ever printing a secret.&lt;/p&gt;

&lt;p&gt;To try it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx secret-blast-radius            &lt;span class="c"&gt;# scan and print findings plus the rotation order&lt;/span&gt;
npx secret-blast-radius &lt;span class="nt"&gt;--runbook&lt;/span&gt;  &lt;span class="c"&gt;# Markdown checklist with steps and verify checks&lt;/span&gt;
npx secret-blast-radius &lt;span class="nt"&gt;--harden&lt;/span&gt;   &lt;span class="c"&gt;# read-only audit of settings that would have limited the damage&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Making the next one hurt less
&lt;/h2&gt;

&lt;p&gt;Rotation is cleanup. A few settings reduce what the next payload can reach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;ignore-scripts=true&lt;/code&gt; in &lt;code&gt;~/.npmrc&lt;/code&gt; and allow-list the few packages that need build scripts. pnpm already blocks dependency build scripts by default.&lt;/li&gt;
&lt;li&gt;Add a release-age cooldown so freshly published versions aren't installed straight away. pnpm has &lt;code&gt;minimumReleaseAge&lt;/code&gt;. The tensorlake version was live for under two hours.&lt;/li&gt;
&lt;li&gt;Keep tokens out of plaintext files. &lt;code&gt;gh auth login&lt;/code&gt; uses the OS keyring when one is available. For publishing from CI, use &lt;a href="https://docs.npmjs.com/trusted-publishers" rel="noopener noreferrer"&gt;npm trusted publishing&lt;/a&gt; instead of a long-lived &lt;code&gt;NPM_TOKEN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Don't pass secrets to the &lt;code&gt;npm install&lt;/code&gt; step in CI. Give them only to the steps that need them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat on provenance: it tells you where a package was built, not whether the code is safe. The tensorlake advisory points out that the malicious version carried valid provenance, because the attacker used the project's own workflow.&lt;/p&gt;

&lt;p&gt;Stop the malware, revoke the tokens that let it spread, then rotate the rest from a clean machine.&lt;/p&gt;




&lt;p&gt;Disclosure: I maintain secret-blast-radius.&lt;/p&gt;

</description>
      <category>security</category>
      <category>npm</category>
      <category>node</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
