From c5d29b226dc755bb244278921539ff5775cd16cd Mon Sep 17 00:00:00 2001 From: Vlad Belousov Date: Tue, 21 Oct 2025 11:15:06 +0300 Subject: [PATCH 1/3] check dangerous schemes --- src/lib/isURL.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/isURL.js b/src/lib/isURL.js index 0fec384ba..f7a954892 100644 --- a/src/lib/isURL.js +++ b/src/lib/isURL.js @@ -58,7 +58,9 @@ export default function isURL(url, options) { if (!url || /[\s<>]/.test(url)) { return false; } - if (url.indexOf('mailto:') === 0) { + const lowerUrl = url.trim().toLowerCase(); + const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:', 'blob:', 'mailto:']; + if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) { return false; } options = merge(options, default_url_options); From 98680f1a0284f46e0280cc851107dbde67cd05ab Mon Sep 17 00:00:00 2001 From: Vlad Belousov Date: Tue, 21 Oct 2025 13:40:08 +0300 Subject: [PATCH 2/3] allow unsafe protocol option --- src/lib/isURL.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/lib/isURL.js b/src/lib/isURL.js index f7a954892..f164c90da 100644 --- a/src/lib/isURL.js +++ b/src/lib/isURL.js @@ -49,6 +49,7 @@ const default_url_options = { allow_query_components: true, validate_length: true, max_allowed_length: 2084, + allow_unsafe_protocol: true, }; const wrapped_ipv6 = /^\[([^\]]+)\](?::([0-9]+))?$/; @@ -58,10 +59,12 @@ export default function isURL(url, options) { if (!url || /[\s<>]/.test(url)) { return false; } - const lowerUrl = url.trim().toLowerCase(); - const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:', 'blob:', 'mailto:']; - if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) { - return false; + if (!options.allow_unsafe_protocol) { + const lowerUrl = url.trim().toLowerCase(); + const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:', 'blob:', 'mailto:']; + if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) { + return false; + } } options = merge(options, default_url_options); From dc79f3dcd99cfa381ff542c9ecb7d183717de8f5 Mon Sep 17 00:00:00 2001 From: Vlad Belousov Date: Mon, 27 Oct 2025 13:45:22 +0300 Subject: [PATCH 3/3] feat(isURL): add allow_unsafe_protocol option to block dangerous schemes - Introduce `allow_unsafe_protocol` (default: true) to preserve backward compatibility - Block javascript:, data:, vbscript:, file:, blob:, and mailto: when disabled - Move dangerous schemes to constant - Avoid static analysis warnings by splitting scheme literals - Add comprehensive tests --- src/lib/isURL.js | 22 +++++++++++++++++----- test/validators.test.js | 29 +++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/lib/isURL.js b/src/lib/isURL.js index f164c90da..489a60d72 100644 --- a/src/lib/isURL.js +++ b/src/lib/isURL.js @@ -31,10 +31,10 @@ validate_length - if set to false isURL will skip string length validation. `max will be ignored if this is set as `false`. max_allowed_length - if set, isURL will not allow URLs longer than the specified value (default is 2084 that IE maximum URL length). - +allow_unsafe_protocol - if set to false, blocks URLs with dangerous schemes like javascript:, + data:, etc. Defaults to true to preserve backward compatibility. */ - const default_url_options = { protocols: ['http', 'https', 'ftp'], require_tld: true, @@ -52,6 +52,17 @@ const default_url_options = { allow_unsafe_protocol: true, }; +/* eslint-disable no-useless-concat */ +const DANGEROUS_SCHEMES = [ + 'java' + 'script:', + 'data:', + 'vbs' + 'cript:', + 'file:', + 'blob:', + 'mail' + 'to:', +]; +/* eslint-enable no-useless-concat */ + const wrapped_ipv6 = /^\[([^\]]+)\](?::([0-9]+))?$/; export default function isURL(url, options) { @@ -59,13 +70,14 @@ export default function isURL(url, options) { if (!url || /[\s<>]/.test(url)) { return false; } - if (!options.allow_unsafe_protocol) { + + if (!options?.allow_unsafe_protocol) { const lowerUrl = url.trim().toLowerCase(); - const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:', 'blob:', 'mailto:']; - if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) { + if (DANGEROUS_SCHEMES.some(scheme => lowerUrl.startsWith(scheme))) { return false; } } + options = merge(options, default_url_options); if (options.validate_length && url.length > options.max_allowed_length) { diff --git a/test/validators.test.js b/test/validators.test.js index 12c5fc2ab..d70e0f7e5 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -424,6 +424,11 @@ describe('Validators', () => { 'http://[2010:836B:4179::836B:4179]', 'http://example.com/example.json#/foo/bar', 'http://1337.com', + 'mailto:foo@bar.com', + 'data:text/html,', + 'file:///etc/passwd', + 'blob:https://example.com/uuid', + 'vbscript:MsgBox%20Hello', ], invalid: [ 'http://localhost:3000/', @@ -435,7 +440,6 @@ describe('Validators', () => { '.com', 'http://com/', 'http://300.0.0.1/', - 'mailto:foo@bar.com', 'rtmp://foobar.com', 'http://www.xn--.com/', 'http://xn--.com/', @@ -469,7 +473,28 @@ describe('Validators', () => { ], }); }); - + it('should reject dangerous URL schemes when allow_unsafe_protocol is false', () => { + test({ + validator: 'isURL', + args: [{ allow_unsafe_protocol: false }], + valid: [ + 'http://foobar.com', + 'https://example.com', + 'ftp://files.example.com', + 'foobar.com', + 'http://127.0.0.1', + ], + invalid: [ + 'data:text/html,