VYPR
Moderate severityNVD Advisory· Published Sep 30, 2025· Updated Sep 30, 2025

CVE-2025-56200

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.

PackageAffected versionsPatched versions
validatornpm
< 13.15.2013.15.20

Affected products

2

Patches

1
cbef5088f02d

fix(isURL): improve protocol detection. Resolves CVE-2025-56200 (#2608)

https://github.com/validatorjs/validator.jsThéo FIDRYOct 21, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.