From 403db691664b1cc509946a3e2d27f50d916e9d7e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 23 Jul 2026 04:05:20 +0000 Subject: [PATCH 1/2] fix: reject credentialed URLs for IPv6, punycode, and text fragments url-regex-safe can match a substring after `@` when exact matching is disabled, so userinfo-bearing URLs were accepted even though the common exact-match path rejects them. Reject username/password up front. --- index.js | 7 ++++++- test/index.js | 7 +++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/index.js b/index.js index 0d0de6e..da3bf57 100644 --- a/index.js +++ b/index.js @@ -7,8 +7,13 @@ const REGEX_HTTP_PROTOCOL = /^https?:\/\//i module.exports = url => { try { - const { href, hostname, hash } = new URL(url) + const { href, hostname, hash, username, password } = new URL(url) if (!REGEX_HTTP_PROTOCOL.test(href)) return false + // Reject credentialed URLs explicitly. When `exact` is false (IPv6, + // punycode, text fragments), url-regex-safe can match a substring after + // `@` while we still return the original href — which would otherwise + // accept userinfo that the exact-match path rejects. + if (username || password) return false const isIPv6 = hostname.startsWith('[') && hostname.endsWith(']') const isPunycode = punycodeRegex.test(hostname) const hasTextFragment = hash.startsWith('#:~:text=') diff --git a/test/index.js b/test/index.js index 0b2b0fd..d13042b 100644 --- a/test/index.js +++ b/test/index.js @@ -51,6 +51,13 @@ const test = require('ava').default 'http://Http://xn--80a0aaa.xn--p1ai', 'http://Http://kikobeats.com', 'https://admin:admin@test-http-login.vercel.app', + // credentialed URLs must stay rejected even when exact-match is + // disabled for text fragments, punycode hosts, or IPv6 + 'https://trusted.example@example.com/#:~:text=x', + 'https://user:pass@example.com/#:~:text=Example', + 'https://trusted.example@xn--80a0aaa.com/', + 'http://trusted.example@[::1]/', + 'http://user:pass@[::1]/', 'http:!!!\0', 'http://-kikobeats.com' ]) From ac229206c5b51c219693bbf1df2bfda94db87fda Mon Sep 17 00:00:00 2001 From: Kiko Beats Date: Wed, 5 Aug 2026 11:23:28 +0200 Subject: [PATCH 2/2] refactor: fold the credential guard into the protocol check Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01MTbj6CYXGsk6rhXx7J8SUP --- index.js | 7 +------ test/index.js | 3 --- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/index.js b/index.js index da3bf57..9547071 100644 --- a/index.js +++ b/index.js @@ -8,12 +8,7 @@ const REGEX_HTTP_PROTOCOL = /^https?:\/\//i module.exports = url => { try { const { href, hostname, hash, username, password } = new URL(url) - if (!REGEX_HTTP_PROTOCOL.test(href)) return false - // Reject credentialed URLs explicitly. When `exact` is false (IPv6, - // punycode, text fragments), url-regex-safe can match a substring after - // `@` while we still return the original href — which would otherwise - // accept userinfo that the exact-match path rejects. - if (username || password) return false + if (!REGEX_HTTP_PROTOCOL.test(href) || username || password) return false const isIPv6 = hostname.startsWith('[') && hostname.endsWith(']') const isPunycode = punycodeRegex.test(hostname) const hasTextFragment = hash.startsWith('#:~:text=') diff --git a/test/index.js b/test/index.js index d13042b..9ffc16c 100644 --- a/test/index.js +++ b/test/index.js @@ -51,12 +51,9 @@ const test = require('ava').default 'http://Http://xn--80a0aaa.xn--p1ai', 'http://Http://kikobeats.com', 'https://admin:admin@test-http-login.vercel.app', - // credentialed URLs must stay rejected even when exact-match is - // disabled for text fragments, punycode hosts, or IPv6 'https://trusted.example@example.com/#:~:text=x', 'https://user:pass@example.com/#:~:text=Example', 'https://trusted.example@xn--80a0aaa.com/', - 'http://trusted.example@[::1]/', 'http://user:pass@[::1]/', 'http:!!!\0', 'http://-kikobeats.com'