diff --git a/src/lib/isURL.js b/src/lib/isURL.js index b55f8e031..600a91dec 100644 --- a/src/lib/isURL.js +++ b/src/lib/isURL.js @@ -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',