From 088c0ff33db6bd981d181f28dec3e8a7d90fa2c4 Mon Sep 17 00:00:00 2001 From: Michael Herzner Date: Tue, 22 Aug 2023 23:31:35 +0200 Subject: [PATCH] feat: add URL validation (#443) closes #378. adds some naive url validations to the submit route. adds some tests for these naive validations. these validations check that: - the urls start w/ `http:` or `https:` - the urls don't use local ip addresses and/or `localhost` --------- Co-authored-by: Asher Gomez --- routes/submit/index.tsx | 6 ++++-- utils/url_validation.ts | 29 +++++++++++++++++++++++++++++ utils/url_validation_test.ts | 24 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 utils/url_validation.ts create mode 100644 utils/url_validation_test.ts 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); +});