fix: resolve TODO.md security/perf/test-coverage backlog
Fixes the 13-item codebase health scan backlog: wraps meal-plan generation in a transaction, adds missing userId/GIN indexes, fixes an IPv6-parsing gap in the webhook SSRF guard (and an identical duplicated bug in the AI URL-import path, now consolidated onto one implementation), paginates the collections list, dedupes the AI recipe Zod schemas, wires up Stripe tier sync, rate-limits AI key rotation, gets `pnpm typecheck` actually working, and adds test coverage for the previously-untested admin/webhooks routes. Two flagged issues (collection removeRecipeId IDOR, tier-limit race) turned out to already be fixed/non-issues on inspection — noted in TODO.md rather than silently dropped. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,24 +1,91 @@
|
||||
import dns from "node:dns/promises";
|
||||
import net from "node:net";
|
||||
|
||||
function isPrivateAddress(ip: string): boolean {
|
||||
const v4 = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (v4) {
|
||||
const [, a, b, c] = v4.map(Number) as [number, number, number, number, number];
|
||||
if (a === 127) return true;
|
||||
if (a === 10) return true;
|
||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
||||
if (a === 192 && b === 168) return true;
|
||||
if (a === 169 && b === 254) return true;
|
||||
if (a >= 224) return true;
|
||||
return false;
|
||||
function isPrivateV4(a: number, b: number): boolean {
|
||||
if (a === 127) return true;
|
||||
if (a === 10) return true;
|
||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
||||
if (a === 192 && b === 168) return true;
|
||||
if (a === 169 && b === 254) return true;
|
||||
if (a >= 224) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Expands a valid IPv6 address (any compression form) into 8 16-bit groups as a BigInt. */
|
||||
function ipv6ToBigInt(ip: string): bigint | null {
|
||||
if (net.isIPv6(ip) !== true) return null;
|
||||
|
||||
const [head, tail] = ip.split("::");
|
||||
const headParts = head ? head.split(":") : [];
|
||||
const tailParts = tail ? tail.split(":") : [];
|
||||
|
||||
// An embedded IPv4 tail (e.g. "::ffff:127.0.0.1") occupies the last two hextets.
|
||||
const expand = (parts: string[]): string[] => {
|
||||
const last = parts[parts.length - 1];
|
||||
if (last && last.includes(".")) {
|
||||
const octets = last.split(".").map(Number);
|
||||
if (octets.length !== 4 || octets.some((o) => !Number.isInteger(o) || o < 0 || o > 255)) {
|
||||
return [];
|
||||
}
|
||||
const hex1 = ((octets[0]! << 8) | octets[1]!).toString(16);
|
||||
const hex2 = ((octets[2]! << 8) | octets[3]!).toString(16);
|
||||
return [...parts.slice(0, -1), hex1, hex2];
|
||||
}
|
||||
return parts;
|
||||
};
|
||||
|
||||
const expandedHead = expand(headParts);
|
||||
const expandedTail = expand(tailParts);
|
||||
|
||||
let groups: string[];
|
||||
if (ip.includes("::")) {
|
||||
const missing = 8 - (expandedHead.length + expandedTail.length);
|
||||
if (missing < 0) return null;
|
||||
groups = [...expandedHead, ...Array(missing).fill("0"), ...expandedTail];
|
||||
} else {
|
||||
groups = expandedHead;
|
||||
}
|
||||
|
||||
if (groups.length !== 8) return null;
|
||||
|
||||
let result = BigInt(0);
|
||||
for (const g of groups) {
|
||||
const val = parseInt(g || "0", 16);
|
||||
if (Number.isNaN(val)) return null;
|
||||
result = (result << BigInt(16)) | BigInt(val);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function isPrivateAddress(ip: string): boolean {
|
||||
const v4 = ip.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/);
|
||||
if (v4) {
|
||||
const octets = v4.slice(1, 5).map(Number);
|
||||
if (octets.some((o) => o > 255)) return true; // malformed, fail closed
|
||||
const [a, b] = octets as [number, number];
|
||||
return isPrivateV4(a, b);
|
||||
}
|
||||
|
||||
const addr = ipv6ToBigInt(ip);
|
||||
if (addr === null) return true; // unparseable, fail closed
|
||||
|
||||
if (addr === BigInt(0) || addr === BigInt(1)) return true; // :: and ::1
|
||||
|
||||
const fc00 = BigInt(0xfc00) << BigInt(112);
|
||||
const fe80 = BigInt(0xfe80) << BigInt(112);
|
||||
const mask7 = BigInt(0xfe00) << BigInt(112); // /7 mask for fc00::/7 (top 7 bits of the address)
|
||||
const mask10 = BigInt(0xffc0) << BigInt(112); // /10 mask for fe80::/10 (top 10 bits of the address)
|
||||
if ((addr & mask7) === (fc00 & mask7)) return true; // unique local fc00::/7
|
||||
if ((addr & mask10) === (fe80 & mask10)) return true; // link-local fe80::/10
|
||||
|
||||
// IPv4-mapped ::ffff:0:0/96
|
||||
if (addr >> BigInt(32) === BigInt(0xffff)) {
|
||||
const embedded = addr & BigInt(0xffffffff);
|
||||
const a = Number((embedded >> BigInt(24)) & BigInt(0xff));
|
||||
const b = Number((embedded >> BigInt(16)) & BigInt(0xff));
|
||||
return isPrivateV4(a, b);
|
||||
}
|
||||
|
||||
const lower = ip.toLowerCase();
|
||||
if (lower === "::1" || lower === "::") return true;
|
||||
if (lower.startsWith("fe80:")) return true;
|
||||
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
||||
const v4mapped = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
||||
if (v4mapped) return isPrivateAddress(v4mapped[1]!);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user