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:
@@ -0,0 +1,37 @@
|
||||
// Tests for server/anon-trial.js — focused on getClientIp, which underpins
|
||||
// the per-IP trial cap. The DB-backed minting paths need a multi-mode SQLite
|
||||
// handle and are exercised by integration tests; here we lock down that the
|
||||
// client IP is taken from Express's trust-proxy-resolved req.ip, never from a
|
||||
// raw client-supplied X-Forwarded-For header.
|
||||
|
||||
import { test, describe } from "node:test";
|
||||
import { strict as assert } from "node:assert";
|
||||
import { getClientIp } from "../anon-trial.js";
|
||||
|
||||
describe("getClientIp", () => {
|
||||
test("uses req.ip (Express's trust-proxy-resolved client address)", () => {
|
||||
assert.equal(getClientIp({ ip: "203.0.113.7" }), "203.0.113.7");
|
||||
});
|
||||
|
||||
test("strips the IPv4-mapped IPv6 prefix", () => {
|
||||
assert.equal(getClientIp({ ip: "::ffff:203.0.113.7" }), "203.0.113.7");
|
||||
});
|
||||
|
||||
test("falls back to the socket address when req.ip is absent", () => {
|
||||
assert.equal(
|
||||
getClientIp({ socket: { remoteAddress: "::ffff:198.51.100.9" } }),
|
||||
"198.51.100.9",
|
||||
);
|
||||
});
|
||||
|
||||
test("does NOT trust a raw client-supplied X-Forwarded-For header", () => {
|
||||
// Express, not getClientIp, decides the client IP from trust proxy. A
|
||||
// header Express hasn't blessed must be ignored — so with no req.ip we
|
||||
// fall through to the socket address, never the spoofed header value.
|
||||
const spoofed = {
|
||||
headers: { "x-forwarded-for": "1.2.3.4" },
|
||||
socket: { remoteAddress: "203.0.113.7" },
|
||||
};
|
||||
assert.equal(getClientIp(spoofed), "203.0.113.7");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user