From 71986fabe3e21ac1095c7f70873fe00b8bfe64bb Mon Sep 17 00:00:00 2001 From: zhongliang02 Date: Tue, 5 Nov 2024 11:11:15 +0800 Subject: [PATCH 1/2] add disallow hostnames option --- packages/validators/src/__tests__/url.test.ts | 44 +++++++++++++++++++ packages/validators/src/url/options.ts | 9 ++++ packages/validators/src/url/utils.ts | 15 +++++-- 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/packages/validators/src/__tests__/url.test.ts b/packages/validators/src/__tests__/url.test.ts index 3c739c4..fcda843 100644 --- a/packages/validators/src/__tests__/url.test.ts +++ b/packages/validators/src/__tests__/url.test.ts @@ -74,6 +74,49 @@ describe('UrlValidator with custom host whitelist', () => { }) }) +describe('UrlValidator with disallowHostnames', () => { + const validator = new UrlValidator({ + whitelist: { + protocols: ['http', 'https'], + disallowHostnames: true + }, + }) + + it('should not throw an error with a proper domain', () => { + expect(() => validator.parse('https://example.com')).not.toThrow() + }) + + it('should throw an error with a hostname', () => { + expect(() => validator.parse('https://tld')).toThrow(UrlValidationError) + expect(() => validator.parse('https://.tld')).toThrow(UrlValidationError) + expect(() => validator.parse('https://tld.')).toThrow(UrlValidationError) + expect(() => validator.parse('https://.tld.')).toThrow(UrlValidationError) + expect(() => validator.parse('https://tld/')).toThrow(UrlValidationError) + expect(() => validator.parse('https://.tld/')).toThrow(UrlValidationError) + expect(() => validator.parse('https://tld./')).toThrow(UrlValidationError) + expect(() => validator.parse('https://.tld./')).toThrow(UrlValidationError) + }) +}) + + +describe('UrlValidator with both hosts and disallowHostnames', () => { + const validator = new UrlValidator({ + whitelist: { + protocols: ['http', 'https'], + hosts: ['example.com', 'localhost'], + disallowHostnames: true + }, + }) + + it('should not throw an error when the host is on the whitelist', () => { + expect(() => validator.parse('https://example.com')).not.toThrow() + }) + + it('should ignore the disallowHostnames option', () => { + expect(() => validator.parse('https://localhost')).not.toThrow() + }) +}) + describe('UrlValidator with base URL', () => { const validator = new UrlValidator({ baseOrigin: 'https://example.com', @@ -144,6 +187,7 @@ describe('createUrlSchema', () => { whitelist: { protocols: ['http', 'https'], hosts: ['example.com'], + disallowHostnames: true }, }), ).not.toThrow() diff --git a/packages/validators/src/url/options.ts b/packages/validators/src/url/options.ts index 63d0e89..0203206 100644 --- a/packages/validators/src/url/options.ts +++ b/packages/validators/src/url/options.ts @@ -3,6 +3,7 @@ import { z } from 'zod' export const defaultOptions = { whitelist: { protocols: ['http', 'https'], + disallowHostnames: false }, } @@ -19,6 +20,14 @@ export const whitelistSchema = z.object({ * It is recommended to provide a list of allowed hostnames to prevent open redirects. */ hosts: z.array(z.string()).optional(), + /** + * Whether to disallow hostnames as valid URLs. + * For example, if disallowHostnames is set to `true`, https://localhost/somepath will be invalid. + * This option is IGNORED if hosts is provided. + * + * @defaultValue false + */ + disallowHostnames: z.boolean().optional() }) /** diff --git a/packages/validators/src/url/utils.ts b/packages/validators/src/url/utils.ts index 95b3268..f90449f 100644 --- a/packages/validators/src/url/utils.ts +++ b/packages/validators/src/url/utils.ts @@ -2,6 +2,7 @@ import { UrlValidationError } from '@/url/errors' import { UrlValidatorWhitelist } from '@/url/options' const DYNAMIC_ROUTE_SEGMENT_REGEX = /\[\[?([^\]]+)\]?\]/g +const IS_NOT_HOSTNAME_REGEX = /[^.]+\.[^.]+/g export const resolveRelativeUrl = (url: string, baseOrigin?: URL): URL => { if (!baseOrigin) { @@ -45,10 +46,18 @@ export const isSafeUrl = (url: URL, whitelist: UrlValidatorWhitelist) => { if (!whitelist.protocols.some(protocol => url.protocol === `${protocol}:`)) { return false } - // only allow whitelisted hosts - if (whitelist.hosts && !whitelist.hosts.some(host => url.host === host)) { - return false + if (whitelist.hosts) { + // only allow whitelisted hosts + if (!whitelist.hosts.some(host => url.host === host)) { + return false + } + } else { + // no hosts provided + if (whitelist.disallowHostnames && !url.host.match(IS_NOT_HOSTNAME_REGEX)) { + return false + } } + // don't allow dynamic routes if (resolveNextDynamicRoute(url).href !== url.href) { return false From 95824f1da5ba57852b258ca2ab986528cba48858 Mon Sep 17 00:00:00 2001 From: zhongliang02 Date: Tue, 5 Nov 2024 11:26:19 +0800 Subject: [PATCH 2/2] linting --- packages/validators/src/__tests__/url.test.ts | 7 +++---- packages/validators/src/url/options.ts | 6 +++--- packages/validators/src/url/utils.ts | 2 +- 3 files changed, 7 insertions(+), 8 deletions(-) diff --git a/packages/validators/src/__tests__/url.test.ts b/packages/validators/src/__tests__/url.test.ts index fcda843..b11ef3f 100644 --- a/packages/validators/src/__tests__/url.test.ts +++ b/packages/validators/src/__tests__/url.test.ts @@ -78,7 +78,7 @@ describe('UrlValidator with disallowHostnames', () => { const validator = new UrlValidator({ whitelist: { protocols: ['http', 'https'], - disallowHostnames: true + disallowHostnames: true, }, }) @@ -98,13 +98,12 @@ describe('UrlValidator with disallowHostnames', () => { }) }) - describe('UrlValidator with both hosts and disallowHostnames', () => { const validator = new UrlValidator({ whitelist: { protocols: ['http', 'https'], hosts: ['example.com', 'localhost'], - disallowHostnames: true + disallowHostnames: true, }, }) @@ -187,7 +186,7 @@ describe('createUrlSchema', () => { whitelist: { protocols: ['http', 'https'], hosts: ['example.com'], - disallowHostnames: true + disallowHostnames: true, }, }), ).not.toThrow() diff --git a/packages/validators/src/url/options.ts b/packages/validators/src/url/options.ts index 0203206..e44961d 100644 --- a/packages/validators/src/url/options.ts +++ b/packages/validators/src/url/options.ts @@ -3,7 +3,7 @@ import { z } from 'zod' export const defaultOptions = { whitelist: { protocols: ['http', 'https'], - disallowHostnames: false + disallowHostnames: false, }, } @@ -24,10 +24,10 @@ export const whitelistSchema = z.object({ * Whether to disallow hostnames as valid URLs. * For example, if disallowHostnames is set to `true`, https://localhost/somepath will be invalid. * This option is IGNORED if hosts is provided. - * + * * @defaultValue false */ - disallowHostnames: z.boolean().optional() + disallowHostnames: z.boolean().optional(), }) /** diff --git a/packages/validators/src/url/utils.ts b/packages/validators/src/url/utils.ts index f90449f..4541eac 100644 --- a/packages/validators/src/url/utils.ts +++ b/packages/validators/src/url/utils.ts @@ -57,7 +57,7 @@ export const isSafeUrl = (url: URL, whitelist: UrlValidatorWhitelist) => { return false } } - + // don't allow dynamic routes if (resolveNextDynamicRoute(url).href !== url.href) { return false