Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion src/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
// middleware.ts
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { PEANUT_API_URL } from '@/constants/general.consts'

export function middleware(request: NextRequest) {
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl
const maintenanceMode = false

Expand Down Expand Up @@ -42,6 +43,50 @@ export function middleware(request: NextRequest) {
const redirectUrl = `https://peanut.to/claim?&${promoList[fragment]}`
return NextResponse.redirect(redirectUrl)
}
// Handle QR redirect lookups
if (pathname.startsWith('/qr/')) {
const match = pathname.match(/^\/qr\/([^\/?#]+)/)
const code = match?.[1]

console.log('[QR Redirect] Handling QR code:', code)

if (code && PEANUT_API_URL) {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 3000)

const lookupUrl = `${PEANUT_API_URL}/redirect/lookup?input=${encodeURIComponent(`qr/${code}`)}`
console.log('[QR Redirect] Looking up URL:', lookupUrl)

const res = await fetch(lookupUrl, {
method: 'GET',
cache: 'no-store', // important to not cache this so the live update works fast (or should we?)
signal: controller.signal,
})

clearTimeout(timeoutId)
console.log('[QR Redirect] Response status:', res.status)

if (res.status === 200) {
const data = await res.json().catch(() => null)
const targetUrl = data?.targetUrl
if (typeof targetUrl === 'string' && targetUrl.length > 0) {
console.log('[QR Redirect] Redirecting to:', targetUrl)
return NextResponse.redirect(new URL(targetUrl, request.url))
}
console.log('[QR Redirect] Invalid target URL in response')
Comment on lines +70 to +77
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Prevent open redirects: restrict targetUrl to same-origin relative paths.

Currently any absolute URL from the API will be redirected. Enforce a leading “/” (no “//”) to keep redirects within the app.

Apply this diff:

-                if (res.status === 200) {
-                    const data = await res.json().catch(() => null)
-                    const targetUrl = data?.targetUrl
-                    if (typeof targetUrl === 'string' && targetUrl.length > 0) {
-                        console.log('[QR Redirect] Redirecting to:', targetUrl)
-                        return NextResponse.redirect(new URL(targetUrl, request.url))
-                    }
-                    console.log('[QR Redirect] Invalid target URL in response')
-                }
+                if (res.status === 200) {
+                    const data = await res.json().catch(() => null)
+                    const targetUrl = data?.targetUrl
+                    // Only allow relative app paths to avoid open redirects
+                    if (typeof targetUrl === 'string' && /^\/(?!\/)/.test(targetUrl)) {
+                        console.log('[QR Redirect] Redirecting to:', targetUrl)
+                        return NextResponse.redirect(new URL(targetUrl, request.url))
+                    }
+                    console.log('[QR Redirect] Invalid or external target URL in response')
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (res.status === 200) {
const data = await res.json().catch(() => null)
const targetUrl = data?.targetUrl
if (typeof targetUrl === 'string' && targetUrl.length > 0) {
console.log('[QR Redirect] Redirecting to:', targetUrl)
return NextResponse.redirect(new URL(targetUrl, request.url))
}
console.log('[QR Redirect] Invalid target URL in response')
if (res.status === 200) {
const data = await res.json().catch(() => null)
const targetUrl = data?.targetUrl
// Only allow relative app paths to avoid open redirects
if (typeof targetUrl === 'string' && /^\/(?!\/)/.test(targetUrl)) {
console.log('[QR Redirect] Redirecting to:', targetUrl)
return NextResponse.redirect(new URL(targetUrl, request.url))
}
console.log('[QR Redirect] Invalid or external target URL in response')
}
🤖 Prompt for AI Agents
In src/middleware.ts around lines 70 to 77, the code currently redirects to any
absolute URL returned by the API; change the validation so targetUrl is allowed
only as a same-origin relative path by requiring it to start with a single '/'
and not with '//' (e.g. ensure typeof targetUrl==='string' &&
targetUrl.startsWith('/') && !targetUrl.startsWith('//') && targetUrl.length>1),
then perform the redirect using the existing relative URL resolution; reject or
log any values that fail this check to prevent open redirects.

}
console.log('[QR Redirect] No redirect - falling through to normal routing')
// If 404 or any other status, fall through to normal routing
} catch (e) {
console.error('[QR Redirect] Error during redirect lookup:', e)
// Network error/timeout -> fall through to normal routing
}
} else {
console.log('[QR Redirect] Missing code or API base URL')
}
// If code missing or base missing, fall through
}

// Set headers to disable caching for specified paths
const response = NextResponse.next()
Expand Down Expand Up @@ -82,5 +127,6 @@ export const config = {
'/pay/:path*',
'/p/:path*',
'/link/:path*',
'/qr/:path*',
],
}
Loading