From 603d73ec65911485fb7748dbc1395416a59fb1ed Mon Sep 17 00:00:00 2001 From: Rik Smale <13023439+WikiRik@users.noreply.github.com> Date: Fri, 21 Nov 2025 20:04:08 +0000 Subject: [PATCH 1/2] fix(isURL): handle possible bypass with URL-encoded content --- src/lib/isURL.js | 12 +++++++++--- test/validators.test.js | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/lib/isURL.js b/src/lib/isURL.js index b55f8e031..24f713045 100644 --- a/src/lib/isURL.js +++ b/src/lib/isURL.js @@ -117,8 +117,8 @@ export default function isURL(url, options) { if (!starts_with_slashes) { const first_slash_position = after_colon.indexOf('/'); const before_slash = first_slash_position === -1 - ? after_colon - : after_colon.substring(0, first_slash_position); + ? after_colon + : after_colon.substring(0, first_slash_position); const at_position = before_slash.indexOf('@'); if (at_position !== -1) { @@ -126,7 +126,12 @@ export default function isURL(url, options) { const valid_auth_regex = /^[a-zA-Z0-9\-_.%:]*$/; const is_valid_auth = valid_auth_regex.test(before_at); - if (is_valid_auth) { + // Check if this contains URL-encoded content that could be malicious + // For example: javascript:%61%6c%65%72%74%28%31%29@example.com + // The encoded part decodes to: alert(1) + const has_encoded_content = /%[0-9a-fA-F]{2}/.test(before_at); + + if (is_valid_auth && !has_encoded_content) { // This looks like authentication (e.g., user:password@host), not a protocol if (options.require_protocol) { return false; @@ -135,6 +140,7 @@ export default function isURL(url, options) { // Don't consume the colon; let the auth parsing handle it later } else { // This looks like a malicious protocol (e.g., javascript:alert();@host) + // or URL-encoded protocol handler (e.g., javascript:%61%6c%65%72%74%28%31%29@host) url = cleanUpProtocol(potential_protocol); if (url === false) { diff --git a/test/validators.test.js b/test/validators.test.js index c5ea4dc99..3c605d99a 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -426,7 +426,6 @@ describe('Validators', () => { 'http://1337.com', // TODO: those probably should not be marked as valid URLs; CVE-2025-56200 /* eslint-disable no-script-url */ - 'javascript:%61%6c%65%72%74%28%31%29@example.com', 'http://evil-site.com@example.com/', 'javascript:alert(1)@example.com', /* eslint-enable no-script-url */ @@ -480,6 +479,8 @@ describe('Validators', () => { 'javascript:var a=1; alert(a);@example.com', 'javascript:alert(1)@user@example.com', 'javascript:alert(1)@example.com?q=safe', + 'javascript:%61%6c%65%72%74%28%31%29@example.com', + 'javascript:%22@a.com#";alert(origin)//', 'data:text/html,@example.com', 'vbscript:msgbox("XSS")@example.com', '//evil-site.com/path@example.com', From 6bb78edacc0275b3cdbd18cf2908b37b3f6e718a Mon Sep 17 00:00:00 2001 From: Rik Smale <13023439+WikiRik@users.noreply.github.com> Date: Fri, 21 Nov 2025 21:06:26 +0100 Subject: [PATCH 2/2] style: fix indentation --- src/lib/isURL.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/isURL.js b/src/lib/isURL.js index 24f713045..600a91dec 100644 --- a/src/lib/isURL.js +++ b/src/lib/isURL.js @@ -117,8 +117,8 @@ export default function isURL(url, options) { if (!starts_with_slashes) { const first_slash_position = after_colon.indexOf('/'); const before_slash = first_slash_position === -1 - ? after_colon - : after_colon.substring(0, first_slash_position); + ? after_colon + : after_colon.substring(0, first_slash_position); const at_position = before_slash.indexOf('@'); if (at_position !== -1) {