Skip to content

Commit 60cfbb9

Browse files
author
victhorbi
committed
fix: harden rate-limit IP resolution + guard stale generation in transak checkout catch
- widget-url route: resolveClientIp prefers x-real-ip (written by trusted proxies, unspoofable from the client) over the leftmost x-forwarded-for entry; falls back to 'unknown' bucket when no public IP is available so attackers cannot trivially rotate headers to bypass the rate limit. - useTransakCheckout open(): guard the catch block with genRef.current !== gen so a superseded generation does not overwrite the active generation's error/status (matches the guard already used before instance assignment).
1 parent 580e744 commit 60cfbb9

2 files changed

Lines changed: 29 additions & 4 deletions

File tree

‎examples/next-template/src/app/api/transak/widget-url/route.ts‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,28 @@ function rateLimited(ip: string): boolean {
4848
return false;
4949
}
5050

51+
const PRIVATE_IP_PREFIXES = ['10.', '172.16.', '172.17.', '172.18.', '172.19.', '172.20.', '172.21.', '172.22.', '172.23.', '172.24.', '172.25.', '172.26.', '172.27.', '172.28.', '172.29.', '172.30.', '172.31.', '192.168.', '127.', '169.254.', '::1', 'fc', 'fd'];
52+
53+
function isPrivateIp(ip: string): boolean {
54+
const trimmed = ip.trim().toLowerCase();
55+
return PRIVATE_IP_PREFIXES.some((p) => trimmed.startsWith(p));
56+
}
57+
58+
function resolveClientIp(req: NextRequest): string {
59+
const realIp = req.headers.get('x-real-ip');
60+
if (realIp && !isPrivateIp(realIp)) {
61+
return realIp.trim();
62+
}
63+
const forwardedFor = req.headers.get('x-forwarded-for');
64+
if (forwardedFor) {
65+
const leftmost = forwardedFor.split(',')[0]?.trim();
66+
if (leftmost && !isPrivateIp(leftmost)) {
67+
return leftmost;
68+
}
69+
}
70+
return 'unknown';
71+
}
72+
5173
async function fetchWithTimeout(
5274
url: string,
5375
init: RequestInit,
@@ -123,8 +145,7 @@ export async function POST(req: NextRequest) {
123145
);
124146
}
125147

126-
const forwardedFor = req.headers.get('x-forwarded-for');
127-
const userIp = forwardedFor?.split(',')[0]?.trim() ?? req.headers.get('x-real-ip') ?? '';
148+
const userIp = resolveClientIp(req);
128149

129150
if (rateLimited(userIp)) {
130151
return NextResponse.json(

‎packages/vechain-kit/src/hooks/payments/useTransakCheckout.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,11 @@ export const useTransakCheckout = (
125125
setStatus('processing');
126126
setIsOpen(true);
127127

128+
genRef.current += 1;
129+
const gen = genRef.current;
130+
128131
try {
129132
removeEmbeddedWidget();
130-
genRef.current += 1;
131-
const gen = genRef.current;
132133

133134
const { Transak: TransakSDK } = await import(
134135
'@transak/ui-js-sdk'
@@ -181,6 +182,9 @@ export const useTransakCheckout = (
181182

182183
instanceRef.current = instance;
183184
} catch (err) {
185+
if (genRef.current !== gen) {
186+
return;
187+
}
184188
const error =
185189
err instanceof Error
186190
? err

0 commit comments

Comments
 (0)