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:
+8
-11
@@ -42,18 +42,15 @@ async function getTrialConfig() {
|
||||
};
|
||||
}
|
||||
|
||||
// Crude IPv4-or-IPv6 string extraction. Trusts the X-Forwarded-For
|
||||
// header's first hop because Recap sits behind StartOS's tunnel — the
|
||||
// header is set by the operator's infrastructure, not by clients
|
||||
// directly. If you ever expose the server without a trusted proxy,
|
||||
// revisit this.
|
||||
// Resolve the real client IP. We rely on Express's `trust proxy` setting
|
||||
// (configured in index.js to the number of trusted proxies in front of the
|
||||
// app) so req.ip is the address the trusted proxy observed — NOT a value the
|
||||
// client can spoof by sending their own X-Forwarded-For. This previously took
|
||||
// the first XFF entry verbatim, which a client could forge to mint unlimited
|
||||
// trials. Falls back to the raw socket address if req.ip isn't populated.
|
||||
export function getClientIp(req) {
|
||||
const xff = req.headers?.["x-forwarded-for"];
|
||||
if (xff) {
|
||||
const first = String(xff).split(",")[0].trim();
|
||||
if (first) return first;
|
||||
}
|
||||
return (req.socket?.remoteAddress || "").replace(/^::ffff:/, "");
|
||||
const ip = req.ip || req.socket?.remoteAddress || "";
|
||||
return ip.replace(/^::ffff:/, "");
|
||||
}
|
||||
|
||||
// Expand an IPv6 string to its full 8-group :-separated form with
|
||||
|
||||
Reference in New Issue
Block a user