Fix five P0/P1 security & correctness findings from the full-eval

- Arbitrary file write (P0): validate import keys in /api/library/import via
  a now-exported safeFilename(); a ../../ key is skipped, not written out of
  the scope dir.
- SSRF (P0): guard downloadPodcastAudio — reject non-HTTP(S) schemes, block
  IP-literal and DNS-resolved private/link-local/loopback/reserved/multicast
  and embedded-IPv4 IPv6 targets (closes DNS rebinding), cap + resolve redirects.
- ESM require (P1): top-level import of randomBytes in license-purchase.js
  (the inner require threw on the anon purchase-settle path).
- Concurrency lock (P1): skip the process-global free-tier slot in multi-mode
  so it no longer serializes every cloud tenant onto one job.
- X-Forwarded-For bypass (P1): set Express trust proxy from
  RECAP_TRUSTED_PROXY_HOPS (default 1); getClientIp now reads req.ip instead
  of a client-spoofable XFF entry.

Tests added for safeFilename, the SSRF guard, and getClientIp (119 pass).
Registry blockers deferred (ROADMAP); leaked-key history purge queued.
This commit is contained in:
Keysat
2026-06-15 13:36:40 -05:00
parent 755b100f00
commit d0e98424c1
11 changed files with 317 additions and 49 deletions
+1 -3
View File
@@ -18,6 +18,7 @@
import { Client } from "@keysat/licensing-client";
import * as license from "./license.js";
import { randomBytes } from "crypto";
const KEYSAT_BASE_URL = license.KEYSAT_BASE_URL;
const PRODUCT_SLUG = license.PRODUCT_SLUG;
@@ -416,10 +417,7 @@ async function maybeApplyPendingSignup(invoiceId, licenseKey, req) {
}
// Local UUID helper — same shape we use in auth-routes for new users.
// Avoids a hard import dep just for this one call.
function randomUuid() {
// Same crypto.randomBytes(16).toString("hex") pattern used elsewhere.
// eslint-disable-next-line global-require
const { randomBytes } = require("crypto");
return randomBytes(16).toString("hex");
}