diff --git a/apps/web/src/app/api/modules/[slug]/review/route.ts b/apps/web/src/app/api/modules/[slug]/review/route.ts index 765fc5f..c6d12a2 100644 --- a/apps/web/src/app/api/modules/[slug]/review/route.ts +++ b/apps/web/src/app/api/modules/[slug]/review/route.ts @@ -13,7 +13,8 @@ export async function GET( ) { const { slug } = await context.params; const { searchParams } = new URL(request.url); - const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); + const rawPage = parseInt(searchParams.get("page") || "1", 10); + const page = Number.isNaN(rawPage) || rawPage < 1 ? 1 : rawPage; const limit = 20; const offset = (page - 1) * limit; diff --git a/apps/web/src/app/api/modules/route.ts b/apps/web/src/app/api/modules/route.ts index 2a56e03..0439bd1 100644 --- a/apps/web/src/app/api/modules/route.ts +++ b/apps/web/src/app/api/modules/route.ts @@ -26,8 +26,10 @@ export async function GET(request: NextRequest) { const search = searchParams.get("search") || ""; const category = searchParams.get("category") || ""; const sort = searchParams.get("sort") || "newest"; // newest | popular | top-rated - const page = Math.max(1, parseInt(searchParams.get("page") || "1", 10)); - const limit = Math.min(50, Math.max(1, parseInt(searchParams.get("limit") || "20", 10))); + const _p = parseInt(searchParams.get("page") || "1", 10); + const page = Number.isNaN(_p) ? 1 : Math.max(1, _p); + const _l = parseInt(searchParams.get("limit") || "20", 10); + const limit = Number.isNaN(_l) ? 20 : Math.min(50, Math.max(1, _l)); const offset = (page - 1) * limit; const sb = getSupabaseAdmin(); diff --git a/apps/web/src/app/api/scan/route.ts b/apps/web/src/app/api/scan/route.ts index ef76b03..99a2666 100644 --- a/apps/web/src/app/api/scan/route.ts +++ b/apps/web/src/app/api/scan/route.ts @@ -1,4 +1,6 @@ import { NextRequest, NextResponse } from "next/server"; +import { isIP } from "net"; +import dns from "dns/promises"; const SECURITY_HEADERS = [ { @@ -35,6 +37,16 @@ function computeGrade(score: number): string { return "F"; } +async function isPrivateIP(hostname: string): Promise { + try { + const ip = isIP(hostname) ? hostname : (await dns.lookup(hostname)).address; + return /^(127\.|10\.|172\.(1[6-9]|2\d|3[01])\.|192\.168\.|169\.254\.|::1|fd[0-9a-f]{2}:)/i.test(ip); + } catch { + // If DNS resolution fails, fail closed (treat as non-resolvable / potentially malicious) + return true; + } +} + /** * POST /api/scan * Free security header scanner — no auth required. @@ -63,6 +75,10 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: "URL must be http or https" }, { status: 400 }); } + if (await isPrivateIP(parsedUrl.hostname)) { + return NextResponse.json({ error: "Scanning internal addresses is not allowed" }, { status: 400 }); + } + try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15000);