diff --git a/routes/submit/index.tsx b/routes/submit/index.tsx index 43f960f2c967..e35656d5192e 100644 --- a/routes/submit/index.tsx +++ b/routes/submit/index.tsx @@ -12,6 +12,7 @@ import Head from "@/components/Head.tsx"; import IconCheckCircle from "tabler_icons_tsx/circle-check.tsx"; import IconCircleX from "tabler_icons_tsx/circle-x.tsx"; import { SignedInState } from "@/utils/middleware.ts"; +import { isPublicUrl, isValidUrl } from "@/utils/url_validation.ts"; export const handler: Handlers = { async POST(req, ctx) { @@ -24,8 +25,9 @@ export const handler: Handlers = { } try { - // Throws if an invalid URL - new URL(url); + if (!isValidUrl(url) || !isPublicUrl(url)) { + return new Response(null, { status: 400 }); + } } catch { return new Response(null, { status: 400 }); } diff --git a/utils/url_validation.ts b/utils/url_validation.ts new file mode 100644 index 000000000000..519cb4e345e5 --- /dev/null +++ b/utils/url_validation.ts @@ -0,0 +1,29 @@ +// Copyright 2023 the Deno authors. All rights reserved. MIT license. + +export function isValidUrl(string: string): boolean { + try { + const { protocol } = new URL(string); + return protocol.startsWith("http"); + } catch { + return false; + } +} + +export function isPublicUrl(string: string): boolean { + try { + const { hostname } = new URL(string); + const ranges = [ + /^localhost$/, + /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, + /^::1$/, + /^0:0:0:0:0:0:0:1$/, + /^10\.\d{1,3}\.\d{1,3}\.\d{1,3}$/, + /^172\.(1[6-9]|2\d|3[0-1])\.\d{1,3}\.\d{1,3}$/, + /^192\.168\.\d{1,3}\.\d{1,3}$/, + ]; + + return !ranges.some((range) => range.test(hostname)); + } catch (_) { + return false; + } +} diff --git a/utils/url_validation_test.ts b/utils/url_validation_test.ts new file mode 100644 index 000000000000..c10597cf3912 --- /dev/null +++ b/utils/url_validation_test.ts @@ -0,0 +1,24 @@ +// Copyright 2023 the Deno authors. All rights reserved. MIT license. + +import { assertEquals } from "std/testing/asserts.ts"; +import { isPublicUrl, isValidUrl } from "./url_validation.ts"; + +Deno.test("[url_validation] isValidUrl()", () => { + assertEquals(isValidUrl("https://hunt.deno.land/"), true); + assertEquals(isValidUrl("http://hunt.deno.land/"), true); + assertEquals(isValidUrl("ws://hunt.deno.land/"), false); + assertEquals(isValidUrl("wss://hunt.deno.land/"), false); + assertEquals(isValidUrl("invalidurl"), false); +}); + +Deno.test("[url_validation] isPublicUrl()", () => { + assertEquals(isPublicUrl("https://hunt.deno.land/"), true); + assertEquals(isPublicUrl("http://hunt.deno.land/"), true); + assertEquals(isPublicUrl("ws://hunt.deno.land/"), true); + assertEquals(isPublicUrl("http://localhost/"), false); + assertEquals(isPublicUrl("http://127.0.0.1/"), false); + assertEquals(isPublicUrl("http://::1/"), false); + assertEquals(isPublicUrl("http://10.0.0.0/"), false); + assertEquals(isPublicUrl("http://172.16.0.0/"), false); + assertEquals(isPublicUrl("http://192.168.0.0/"), false); +});