diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index af090a9be..e0024eff3 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
- node-version: [22, 20, 18, 16, 14, 12, 10, 8, 6]
+ node-version: [22, 20, 18, 16, 14, 12, 10, 8]
name: Run tests on Node.js ${{ matrix.node-version }}
steps:
- name: Setup Node.js ${{ matrix.node-version }}
diff --git a/README.md b/README.md
index a0ea7a65d..37fbd5e3d 100644
--- a/README.md
+++ b/README.md
@@ -167,7 +167,7 @@ Validator | Description
**isStrongPassword(str [, options])** | check if the string can be considered a strong password or not. Allows for custom requirements or scoring rules. If `returnScore` is true, then the function returns an integer score for the password rather than a boolean.
Default options:
`{ minLength: 8, minLowercase: 1, minUppercase: 1, minNumbers: 1, minSymbols: 1, returnScore: false, pointsPerUnique: 1, pointsPerRepeat: 0.5, pointsForContainingLower: 10, pointsForContainingUpper: 10, pointsForContainingNumber: 10, pointsForContainingSymbol: 10 }`
**isTime(str [, options])** | check if the string is a valid time e.g. [`23:01:59`, new Date().toLocaleTimeString()].
`options` is an object which can contain the keys `hourFormat` or `mode`.
`hourFormat` is a key and defaults to `'hour24'`.
`mode` is a key and defaults to `'default'`.
`hourFormat` can contain the values `'hour12'` or `'hour24'`, `'hour24'` will validate hours in 24 format and `'hour12'` will validate hours in 12 format.
`mode` can contain the values `'default', 'withSeconds', withOptionalSeconds`, `'default'` will validate `HH:MM` format, `'withSeconds'` will validate the `HH:MM:SS` format, `'withOptionalSeconds'` will validate `'HH:MM'` and `'HH:MM:SS'` formats.
**isTaxID(str, locale)** | check if the string is a valid Tax Identification Number. Default locale is `en-US`.
More info about exact TIN support can be found in `src/lib/isTaxID.js`.
Supported locales: `[ 'bg-BG', 'cs-CZ', 'de-AT', 'de-DE', 'dk-DK', 'el-CY', 'el-GR', 'en-CA', 'en-GB', 'en-IE', 'en-US', 'es-AR', 'es-ES', 'et-EE', 'fi-FI', 'fr-BE', 'fr-CA', 'fr-FR', 'fr-LU', 'hr-HR', 'hu-HU', 'it-IT', 'lb-LU', 'lt-LT', 'lv-LV', 'mt-MT', 'nl-BE', 'nl-NL', 'pl-PL', 'pt-BR', 'pt-PT', 'ro-RO', 'sk-SK', 'sl-SI', 'sv-SE', 'uk-UA']`.
-**isURL(str [, options])** | check if the string is a URL.
`options` is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, require_host: true, require_port: false, require_valid_protocol: true, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false, allow_fragments: true, allow_query_components: true, disallow_auth: false, validate_length: true }`.
`protocols` - valid protocols can be modified with this option.
`require_tld` - If set to false isURL will not check if the URL's host includes a top-level domain.
`require_protocol` - if set to true isURL will return false if protocol is not present in the URL.
`require_host` - if set to false isURL will not check if host is present in the URL.
`require_port` - if set to true isURL will check if port is present in the URL.
`require_valid_protocol` - isURL will check if the URL's protocol is present in the protocols option.
`allow_underscores` - if set to true, the validator will allow underscores in the URL.
`host_whitelist` - if set to an array of strings or regexp, and the domain matches none of the strings defined in it, the validation fails.
`host_blacklist` - if set to an array of strings or regexp, and the domain matches any of the strings defined in it, the validation fails.
`allow_trailing_dot` - if set to true, the validator will allow the domain to end with a `.` character.
`allow_protocol_relative_urls` - if set to true protocol relative URLs will be allowed.
`allow_fragments` - if set to false isURL will return false if fragments are present.
`allow_query_components` - if set to false isURL will return false if query components are present.
`disallow_auth` - if set to true, the validator will fail if the URL contains an authentication component, e.g. `http://username:password@example.com`.
`validate_length` - if set to false isURL will skip string length validation. `max_allowed_length` 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).
+**isURL(str [, options])** | check if the string is a URL.
`options` is an object which defaults to `{ protocols: ['http','https','ftp'], require_tld: true, require_protocol: false, require_host: true, require_port: false, require_valid_protocol: true, allow_underscores: false, host_whitelist: false, host_blacklist: false, allow_trailing_dot: false, allow_protocol_relative_urls: false, allow_fragments: true, allow_query_components: true, disallow_auth: false, validate_length: true }`.
`protocols` - valid protocols can be modified with this option.
`require_tld` - If set to false isURL will not check if the URL's host includes a top-level domain.
`require_protocol` - **RECOMMENDED** if set to true isURL will return false if protocol is not present in the URL. Without this setting, some malicious URLs cannot be distinguishable from a valid URL with authentication information.
`require_host` - if set to false isURL will not check if host is present in the URL.
`require_port` - if set to true isURL will check if port is present in the URL.
`require_valid_protocol` - isURL will check if the URL's protocol is present in the protocols option.
`allow_underscores` - if set to true, the validator will allow underscores in the URL.
`host_whitelist` - if set to an array of strings or regexp, and the domain matches none of the strings defined in it, the validation fails.
`host_blacklist` - if set to an array of strings or regexp, and the domain matches any of the strings defined in it, the validation fails.
`allow_trailing_dot` - if set to true, the validator will allow the domain to end with a `.` character.
`allow_protocol_relative_urls` - if set to true protocol relative URLs will be allowed.
`allow_fragments` - if set to false isURL will return false if fragments are present.
`allow_query_components` - if set to false isURL will return false if query components are present.
`disallow_auth` - if set to true, the validator will fail if the URL contains an authentication component, e.g. `http://username:password@example.com`.
`validate_length` - if set to false isURL will skip string length validation. `max_allowed_length` 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).
**isULID(str)** | check if the string is a [ULID](https://github.com/ulid/spec).
**isUUID(str [, version])** | check if the string is an RFC9562 UUID.
`version` is one of `'1'`-`'8'`, `'nil'`, `'max'`, `'all'` or `'loose'`. The `'loose'` option checks if the string is a UUID-like string with hexadecimal values, ignoring RFC9565.
**isVariableWidth(str)** | check if the string contains a mixture of full and half-width chars.
diff --git a/src/lib/isURL.js b/src/lib/isURL.js
index 0fec384ba..8ae971ed6 100644
--- a/src/lib/isURL.js
+++ b/src/lib/isURL.js
@@ -83,21 +83,94 @@ export default function isURL(url, options) {
split = url.split('?');
url = split.shift();
- split = url.split('://');
- if (split.length > 1) {
- protocol = split.shift().toLowerCase();
+ // Replaced the 'split("://")' logic with a regex to match the protocol.
+ // This correctly identifies schemes like `javascript:` which don't use `//`.
+ // However, we need to be careful not to confuse authentication credentials (user:password@host)
+ // with protocols. A colon before an @ symbol might be part of auth, not a protocol separator.
+ const protocol_match = url.match(/^([a-z][a-z0-9+\-.]*):/i);
+ let had_explicit_protocol = false;
+
+ const cleanUpProtocol = (potential_protocol) => {
+ had_explicit_protocol = true;
+ protocol = potential_protocol.toLowerCase();
+
if (options.require_valid_protocol && options.protocols.indexOf(protocol) === -1) {
+ // The identified protocol is not in the allowed list.
return false;
}
+
+ // Remove the protocol from the URL string.
+ return url.substring(protocol_match[0].length);
+ };
+
+ if (protocol_match) {
+ const potential_protocol = protocol_match[1];
+ const after_colon = url.substring(protocol_match[0].length);
+
+ // Check if what follows looks like authentication credentials (user:password@host)
+ // rather than a protocol. This happens when:
+ // 1. There's no `//` after the colon (protocols like `http://` have this)
+ // 2. There's an `@` symbol before any `/`
+ // 3. The part before `@` contains only valid auth characters (alphanumeric, -, _, ., %, :)
+ const starts_with_slashes = after_colon.slice(0, 2) === '//';
+
+ 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);
+ const at_position = before_slash.indexOf('@');
+
+ if (at_position !== -1) {
+ const before_at = before_slash.substring(0, at_position);
+ const valid_auth_regex = /^[a-zA-Z0-9\-_.%:]*$/;
+ const is_valid_auth = valid_auth_regex.test(before_at);
+
+ if (is_valid_auth) {
+ // This looks like authentication (e.g., user:password@host), not a protocol
+ if (options.require_protocol) {
+ return false;
+ }
+
+ // Don't consume the colon; let the auth parsing handle it later
+ } else {
+ // This looks like a malicious protocol (e.g., javascript:alert();@host)
+ url = cleanUpProtocol(potential_protocol);
+
+ if (url === false) {
+ return false;
+ }
+ }
+ } else {
+ // No @ symbol, this is definitely a protocol
+ url = cleanUpProtocol(potential_protocol);
+
+ if (url === false) {
+ return false;
+ }
+ }
+ } else {
+ // Starts with '//', this is definitely a protocol like http://
+ url = cleanUpProtocol(potential_protocol);
+
+ if (url === false) {
+ return false;
+ }
+ }
} else if (options.require_protocol) {
return false;
- } else if (url.slice(0, 2) === '//') {
- if (!options.allow_protocol_relative_urls) {
+ }
+
+ // Handle leading '//' only as protocol-relative when there was NO explicit protocol.
+ // If there was an explicit protocol, '//' is the normal separator
+ // and should be stripped unconditionally.
+ if (url.slice(0, 2) === '//') {
+ if (!had_explicit_protocol && !options.allow_protocol_relative_urls) {
return false;
}
- split[0] = url.slice(2);
+
+ url = url.slice(2);
}
- url = split.join('://');
if (url === '') {
return false;
diff --git a/test/validators.test.js b/test/validators.test.js
index 12c5fc2ab..a3c5f5a5d 100644
--- a/test/validators.test.js
+++ b/test/validators.test.js
@@ -424,6 +424,12 @@ describe('Validators', () => {
'http://[2010:836B:4179::836B:4179]',
'http://example.com/example.json#/foo/bar',
'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 */
],
invalid: [
'http://localhost:3000/',
@@ -466,6 +472,18 @@ describe('Validators', () => {
'////foobar.com',
'http:////foobar.com',
'https://example.com/foo//',
+ // the following tests are because of CVE-2025-56200
+ /* eslint-disable no-script-url */
+ "javascript:alert(1);a=';@example.com/alert(1)'",
+ 'JaVaScRiPt:alert(1)@example.com',
+ 'javascript:/* comment */alert(1)@example.com',
+ 'javascript:var a=1; alert(a);@example.com',
+ 'javascript:alert(1)@user@example.com',
+ 'javascript:alert(1)@example.com?q=safe',
+ 'data:text/html,@example.com',
+ 'vbscript:msgbox("XSS")@example.com',
+ '//evil-site.com/path@example.com',
+ /* eslint-enable no-script-url */
],
});
});
@@ -478,9 +496,11 @@ describe('Validators', () => {
}],
valid: [
'rtmp://foobar.com',
+ 'rtmp:foobar.com',
],
invalid: [
'http://foobar.com',
+ 'tel:+15551234567',
],
});
});
@@ -533,6 +553,9 @@ describe('Validators', () => {
'rtmp://foobar.com',
'http://foobar.com',
'test://foobar.com',
+ // Dangerous! This allows to mark malicious URLs as a valid URL (CVE-2025-56200)
+ // eslint-disable-next-line no-script-url
+ 'javascript:alert(1);@example.com',
],
invalid: [
'mailto:test@example.com',
@@ -704,6 +727,61 @@ describe('Validators', () => {
});
});
+ it('should validate authentication strings if a protocol is not required', () => {
+ test({
+ validator: 'isURL',
+ args: [{
+ require_protocol: false,
+ }],
+ valid: [
+ 'user:pw@foobar.com/',
+ ],
+ invalid: [
+ 'user:pw,@foobar.com/',
+ ],
+ });
+ });
+
+ it('should reject authentication strings if a protocol is required', () => {
+ test({
+ validator: 'isURL',
+ args: [{
+ require_protocol: true,
+ }],
+ valid: [
+ 'http://user:pw@foobar.com/',
+ 'https://user:password@example.com',
+ 'ftp://admin:pass@ftp.example.com/',
+ ],
+ invalid: [
+ 'user:pw@foobar.com/',
+ 'user:password@example.com',
+ 'admin:pass@ftp.example.com/',
+ ],
+ });
+ });
+
+ it('should reject invalid protocols when require_valid_protocol is enabled', () => {
+ test({
+ validator: 'isURL',
+ args: [{
+ require_valid_protocol: true,
+ protocols: ['http', 'https', 'ftp'],
+ }],
+ valid: [
+ 'http://example.com',
+ 'https://example.com',
+ 'ftp://example.com',
+ ],
+ invalid: [
+ // eslint-disable-next-line no-script-url
+ 'javascript:alert(1);@example.com',
+ 'data:text/html,@example.com',
+ 'file:///etc/passwd@example.com',
+ ],
+ });
+ });
+
it('should let users specify a host whitelist', () => {
test({
validator: 'isURL',
@@ -782,6 +860,24 @@ describe('Validators', () => {
});
});
+ it('GHSA-9965-vmph-33xx vulnerability - protocol delimiter parsing difference', () => {
+ const DOMAIN_WHITELIST = ['example.com'];
+
+ test({
+ validator: 'isURL',
+ args: [{
+ protocols: ['https'],
+ host_whitelist: DOMAIN_WHITELIST,
+ require_host: false,
+ }],
+ valid: [],
+ invalid: [
+ // eslint-disable-next-line no-script-url
+ "javascript:alert(1);a=';@example.com/alert(1)",
+ ],
+ });
+ });
+
it('should allow rejecting urls containing authentication information', () => {
test({
validator: 'isURL',