fix: resolve DNS before SSRF check and block redirects in parse-url

isPrivateUrl() did string-only hostname matching and never resolved DNS,
so a public-looking name that maps to an internal IP (e.g.
127-0-0-1.sslip.io -> 127.0.0.1) passed the check while fetch/extract
later resolved it and reached internal services (GHSA-wqcv-5qvx-vx75).

- isPrivateUrl is now async: it keeps the fast string/literal-IP path,
  then resolves the hostname via DNS and rejects if any address is private.
- parse-url now fetches the page itself with redirect: "error" and parses
  via extractFromHtml(), since article-extractor follows redirects
  internally and drops a redirect option, which allowed a public URL to
  302 to an internal host.
- Update validate-model call site to await; add regression tests.
This commit is contained in:
dayuan.jiang
2026-06-27 18:03:28 +09:00
parent 5c884766a8
commit 73862f6108
4 changed files with 182 additions and 109 deletions

View File

@@ -56,7 +56,7 @@ export async function POST(req: Request) {
}
// SECURITY: Block SSRF attacks via custom baseUrl
if (baseUrl && !allowPrivateUrls() && isPrivateUrl(baseUrl)) {
if (baseUrl && !allowPrivateUrls() && (await isPrivateUrl(baseUrl))) {
return NextResponse.json(
{ valid: false, error: "Invalid base URL" },
{ status: 400 },