CVE-2025-56200
Description
A URL validation bypass vulnerability exists in validator.js through version 13.15.15. The isURL() function uses '://' as a delimiter to parse protocols, while browsers use ':' as the delimiter. This parsing difference allows attackers to bypass protocol and domain validation by crafting URLs leading to XSS and Open Redirect attacks.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
In validator.js through 13.15.15, isURL() uses '://' as protocol delimiter while browsers use ':', enabling URL validation bypass leading to XSS and Open Redirect.
Vulnerability
Details The isURL() function in validator.js splits URLs by :// to extract the protocol, whereas browsers parse protocols using : as the delimiter [1][4]. This discrepancy allows an attacker to craft URLs like http:evil.com that pass isURL validation (since :// is absent) but are interpreted as valid URLs by the browser [3]. By default, require_protocol is disabled, further widening the attack surface [4].
Exploitation
An attacker can supply a URL such as javascript:alert(1) or http:attacker.com to a function that relies on isURL for validation. The validator accepts the URL, but when a browser processes it, the javascript: or http: scheme is recognized, leading to XSS or open redirect [3][4]. No authentication is required; the attacker only needs to inject the crafted URL into a vulnerable application using validator.js.
Impact
Successful exploitation enables cross-site scripting (XSS) and open redirect attacks. These can be chained to achieve account takeover (ATO) or, in certain contexts, remote code execution (RCE) [4].
Mitigation
The vulnerability is fixed in commit cbef508 [2]. Users should upgrade to version 13.15.16 or later. No workarounds are available.
AI Insight generated on May 19, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
validatornpm | < 13.15.20 | 13.15.20 |
Affected products
2- validator.js/validator.jsdescription
Patches
1cbef5088f02dfix(isURL): improve protocol detection. Resolves CVE-2025-56200 (#2608)
4 files changed · +178 −9
.github/workflows/ci.yml+1 −1 modified@@ -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 }}
README.md+1 −1 modified@@ -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.<br/>Default options: <br/>`{ 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()].<br/><br/> `options` is an object which can contain the keys `hourFormat` or `mode`.<br/><br/>`hourFormat` is a key and defaults to `'hour24'`.<br/><br/>`mode` is a key and defaults to `'default'`. <br/><br/>`hourFormat` can contain the values `'hour12'` or `'hour24'`, `'hour24'` will validate hours in 24 format and `'hour12'` will validate hours in 12 format. <br/><br/>`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`.<br/><br/>More info about exact TIN support can be found in `src/lib/isTaxID.js`.<br/><br/>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.<br/><br/>`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 }`.<br/><br/>`protocols` - valid protocols can be modified with this option.<br/>`require_tld` - If set to false isURL will not check if the URL's host includes a top-level domain.<br/>`require_protocol` - if set to true isURL will return false if protocol is not present in the URL.<br/>`require_host` - if set to false isURL will not check if host is present in the URL.<br/>`require_port` - if set to true isURL will check if port is present in the URL.<br/>`require_valid_protocol` - isURL will check if the URL's protocol is present in the protocols option.<br/>`allow_underscores` - if set to true, the validator will allow underscores in the URL.<br/>`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.<br/>`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.<br/>`allow_trailing_dot` - if set to true, the validator will allow the domain to end with a `.` character.<br/>`allow_protocol_relative_urls` - if set to true protocol relative URLs will be allowed.<br/>`allow_fragments` - if set to false isURL will return false if fragments are present.<br/>`allow_query_components` - if set to false isURL will return false if query components are present.<br/>`disallow_auth` - if set to true, the validator will fail if the URL contains an authentication component, e.g. `http://username:password@example.com`.<br/>`validate_length` - if set to false isURL will skip string length validation. `max_allowed_length` will be ignored if this is set as `false`.<br/>`max_allowed_length` - if set, isURL will not allow URLs longer than the specified value (default is 2084 that IE maximum URL length).<br/> +**isURL(str [, options])** | check if the string is a URL.<br/><br/>`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 }`.<br/><br/>`protocols` - valid protocols can be modified with this option.<br/>`require_tld` - If set to false isURL will not check if the URL's host includes a top-level domain.<br/>`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.<br/>`require_host` - if set to false isURL will not check if host is present in the URL.<br/>`require_port` - if set to true isURL will check if port is present in the URL.<br/>`require_valid_protocol` - isURL will check if the URL's protocol is present in the protocols option.<br/>`allow_underscores` - if set to true, the validator will allow underscores in the URL.<br/>`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.<br/>`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.<br/>`allow_trailing_dot` - if set to true, the validator will allow the domain to end with a `.` character.<br/>`allow_protocol_relative_urls` - if set to true protocol relative URLs will be allowed.<br/>`allow_fragments` - if set to false isURL will return false if fragments are present.<br/>`allow_query_components` - if set to false isURL will return false if query components are present.<br/>`disallow_auth` - if set to true, the validator will fail if the URL contains an authentication component, e.g. `http://username:password@example.com`.<br/>`validate_length` - if set to false isURL will skip string length validation. `max_allowed_length` will be ignored if this is set as `false`.<br/>`max_allowed_length` - if set, isURL will not allow URLs longer than the specified value (default is 2084 that IE maximum URL length).<br/> **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.<br/>`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.
src/lib/isURL.js+80 −7 modified@@ -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;
test/validators.test.js+96 −0 modified@@ -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/<script>alert(\'XSS\')</script>/', + // 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,<script>alert(1)</script>@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,<script>alert(1)</script>@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',
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/advisories/GHSA-9965-vmph-33xxghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-56200ghsaADVISORY
- validatorjs.comghsaWEB
- gist.github.com/junan-98/27ae092aa40e2a057d41a0f95148f666ghsaWEB
- gist.github.com/junan-98/a93130505b258b9e4ec9f393e7533596ghsaWEB
- github.com/validatorjs/validator.js/commit/cbef5088f02d36caf978f378bb845fe49bdc0809ghsaWEB
- github.com/validatorjs/validator.js/issues/2600ghsaWEB
- github.com/validatorjs/validator.js/pull/2608ghsaWEB
- github.com/validatorjs/validator.js/releases/tag/13.15.20ghsaWEB
News mentions
0No linked articles in our index yet.