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:
+17
-1
@@ -116,6 +116,18 @@ import { buildTenantAuthMiddleware } from "./tenant-auth.js";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
const app = express();
|
||||
|
||||
// Trust the operator's reverse proxy (StartOS / StartTunnel, or a cloud proxy)
|
||||
// so req.ip is the real client address rather than a client-spoofable
|
||||
// X-Forwarded-For entry. The value is how many trusted proxies sit in front of
|
||||
// this process — default 1 (the StartOS/StartTunnel hop). Erring low is safe
|
||||
// (it can only over-count clients onto one IP, hitting the trial cap sooner);
|
||||
// erring high would re-open the trial-cap bypass. Override via
|
||||
// RECAP_TRUSTED_PROXY_HOPS (0 = no proxy in front; use the socket address only).
|
||||
const hopsParsed = parseInt(process.env.RECAP_TRUSTED_PROXY_HOPS, 10);
|
||||
const trustedProxyHops =
|
||||
Number.isInteger(hopsParsed) && hopsParsed >= 0 ? hopsParsed : 1;
|
||||
app.set("trust proxy", trustedProxyHops);
|
||||
const PORT = process.env.PORT || 3001;
|
||||
|
||||
// ── Multi-tenant mode toggle ────────────────────────────────────────────
|
||||
@@ -2618,7 +2630,11 @@ app.post("/api/process", async (req, res) => {
|
||||
// through the relay. Non-relay providers ignore this opt.
|
||||
const jobId = randomUUID();
|
||||
|
||||
const isFree = isFreeUser();
|
||||
// The free-tier single-flight lock is a single-mode concept (one operator,
|
||||
// BYO key, one job at a time). In multi mode, per-tenant credit metering is
|
||||
// the resource control, so a process-global lock would wrongly serialize
|
||||
// every tenant onto one job at a time — never apply it there.
|
||||
const isFree = req.recapMode !== "multi" && isFreeUser();
|
||||
if (isFree) {
|
||||
if (!tryAcquireFreeSlot({ url, title: itemTitle, abortController })) {
|
||||
const current = getCurrentFreeJob();
|
||||
|
||||
Reference in New Issue
Block a user