neverpay is a single-operator, self-hosted application that handles money (crypto receipts), license keys, file delivery, and an admin console. This documents its threat model, the controls in place, and accepted residual risk.
Found a vulnerability? Open a private report via the repository's security advisories, or email the maintainer. Please don't file public issues for exploitable bugs until a fix is out.
Custody / funds
- Non-custodial: only watch-only xpubs / receiving accounts are configured. Private keys never touch neverpay. There is no funds-moving code path.
- A unique pay-to address per order (atomic HD index for EVM/UTXO/Tron; a unique X-address / muxed M-address for XRP/XLM), so payments are unambiguously attributable and addresses aren't reused.
Admin auth
- bcrypt password hashing (cost 12); 256-bit random session tokens; sessions expire (7d) and are validated server-side on every request.
- Session cookie is
HttpOnly,SameSite=Lax, andSecurewhen served over HTTPS (NEVERPAY_BASE_URL=https://…). - Password policy: a 12-character minimum plus a small common-password blocklist,
enforced by
neverpay setupand forNEVERPAY_ADMIN_PASSWORDon first run. The auto-generated default password is 96-bit. (An already-configured install with a now-sub-policyNEVERPAY_ADMIN_PASSWORDis not bricked on upgrade — it logs a warning and keeps the existing password.) POST /admin/loginis rate-limited (10/min/IP) and brute-force throttled: consecutive failures from an IP trigger an escalating, auto-expiring cooldown (capped at 15 min), with a fixed ~750 ms delay on every failed attempt. A correct password is always accepted — even from a throttled IP — and clears the cooldown, so the throttle only delays repeated wrong guesses. Active throttling is visible on/admin/status.
License API (/api/v1)
- Unauthenticated by design — the signed license token is the credential, and
it is not a cookie, so permissive CORS (
*) grants no privilege. - Offline verification uses Ed25519: tampering invalidates the signature.
- Rate-limited (120/min/IP).
Web hardening
html/templateauto-escaping on all rendered data; the only raw-CSS sink (brand accent) is strictly hex-validated.- SQL is fully parameterized (no string-built queries).
- Downloads: 160-bit unguessable token, gated on
fulfilled+ time window; files served viafilepath.Base(no path traversal); uploads stored under a random name. - Security headers:
X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer(keeps download-token URLs out ofReferer), andX-Frame-Options: DENYeverywhere except the embeddable checkout (/p/…,/order/…), which is intentionally framed by seller sites.
Webhooks
- Outbound deliveries are HMAC-SHA256 signed (
X-Neverpay-Signature: t=…,v1=…overt.body); verify with timestamp tolerance to resist replay. - SSRF-guarded: webhook URLs must be http(s) and, by default, may not resolve to
loopback/private/link-local addresses (incl. cloud metadata
169.254.169.254); the check runs at dial time, after DNS, so rebinding can't bypass it, and redirects are not followed. SetNEVERPAY_WEBHOOK_ALLOW_PRIVATE=1to target a same-host/LAN receiver.
Admin CSRF
- The admin session cookie is
SameSite=Lax; in addition, state-changing admin requests (incl. login/logout) are rejected when they are cross-origin (Sec-Fetch-Site, falling back to anOrigin/Refererhost check). The public checkout and licensing API are intentionally cross-origin and unguarded.
Privacy
- No telemetry, no third-party browser calls (system-font stack, embedded assets) — runs cleanly as a Tor hidden service.
- Serve behind HTTPS (or a
.onion). neverpay speaks HTTP; terminate TLS at a reverse proxy and setNEVERPAY_BASE_URLaccordingly. - Behind a proxy, set
NEVERPAY_TRUST_PROXY=1so per-client rate limiting uses the real client IP fromX-Forwarded-For. Do not set it when neverpay is directly exposed —X-Forwarded-Foris then attacker-spoofable. - Choose a strong
NEVERPAY_ADMIN_PASSWORD(or use the generated one). - The SQLite DB holds sensitive secrets — the license signing seed, webhook
secrets, and the admin hash. Protect and back it up (see DEPLOY.md). Losing
the signing seed means previously issued keys can't be verified. The admin
status page (
/admin/status) is session-gated and renders only the public key — the seed is never exposed over HTTP.
- No CSRF tokens on admin forms. Mitigated by
SameSite=Laxsession cookies (browsers don't send the cookie on cross-site POST) plus admin frame-deny. Acceptable for a single-operator console; revisit if multi-user admin lands. - Unbounded activation rows when a key has no seat limit (
seats = 0). A holder of a valid key could create many HWID activations. Bounded by the API rate limit; set a seat limit for keys that need hard caps. - Login rate-limit/throttle is keyed by source IP. On a shared egress (Tor
exit, CGNAT, VPN — set
NEVERPAY_TRUST_PROXY=1behind a reverse proxy to get per-client IPs) the operator and an attacker share one key. The brute-force throttle never blocks a correct password (it's verified before the cooldown is consulted), but a flood from a shared IP can still hit the coarse 10/min/IP request limit; a logged-in operator (7-day session) is unaffected. - Stale pending orders / expired sessions accumulate as rows (rejected on read). Negligible at single-operator scale; prune via DB maintenance if needed.
- EVM detection reads current balance, not cumulative received. Safe given fresh per-order addresses; don't sweep an order's address before fulfillment.