Skip to content

Commit 4676c98

Browse files
authored
Merge branch 'master' into fix(isURL)--GHSA-9965-vmph-33xx
2 parents dc79f3d + 4af6124 commit 4676c98

File tree

7 files changed

+193
-16
lines changed

7 files changed

+193
-16
lines changed

.github/workflows/ci.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
node-version: [22, 20, 18, 16, 14, 12, 10, 8, 6]
12+
node-version: [22, 20, 18, 16, 14, 12, 10, 8]
1313
name: Run tests on Node.js ${{ matrix.node-version }}
1414
steps:
1515
- name: Setup Node.js ${{ matrix.node-version }}

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,16 @@
1+
# 13.15.20
2+
3+
### Fixes, New Locales and Enhancements
4+
5+
- [#2556](https://github.com/validatorjs/validator.js/pull/2556) `isMobilePhone`: add `ar-QA` locale @WardKhaddour
6+
- [#2576](https://github.com/validatorjs/validator.js/pull/2576) `isAlpha`/`isAlphanuneric`: add Indic locales (`ta-IN`, `te-IN`, `kn-IN`, `ml-IN`, `gu-IN`, `pa-IN`, `or-IN`) @avadootharajesh
7+
- [#2574](https://github.com/validatorjs/validator.js/pull/2574) `isBase64`: improve padding regex @KrayzeeKev
8+
- [#2584](https://github.com/validatorjs/validator.js/pull/2584) `isVAT`: improve `FR` locale @iamAmer
9+
- [#2608](https://github.com/validatorjs/validator.js/pull/2608) `isURL`: improve protocol detection. Resolves CVE-2025-56200 @theofidry
10+
- **Doc fixes and others:**
11+
- [#2563](https://github.com/validatorjs/validator.js/pull/2563) @stoneLeaf
12+
- [#2581](https://github.com/validatorjs/validator.js/pull/2581) @camillobruni
13+
114
# 13.15.15
215

316
### Fixes, New Locales and Enhancements

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ Validator | Description
167167
**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 }`
168168
**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.
169169
**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']`.
170-
**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:[email protected]`.<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/>
170+
**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:[email protected]`.<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/>
171171
**isULID(str)** | check if the string is a [ULID](https://github.com/ulid/spec).
172172
**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.
173173
**isVariableWidth(str)** | check if the string contains a mixture of full and half-width chars.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "validator",
33
"description": "String validation and sanitization",
4-
"version": "13.15.15",
4+
"version": "13.15.20",
55
"sideEffects": false,
66
"homepage": "https://github.com/validatorjs/validator.js",
77
"files": [

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ import isStrongPassword from './lib/isStrongPassword';
130130

131131
import isVAT from './lib/isVAT';
132132

133-
const version = '13.15.15';
133+
const version = '13.15.20';
134134

135135
const validator = {
136136
version,

src/lib/isURL.js

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -100,21 +100,94 @@ export default function isURL(url, options) {
100100
split = url.split('?');
101101
url = split.shift();
102102

103-
split = url.split('://');
104-
if (split.length > 1) {
105-
protocol = split.shift().toLowerCase();
103+
// Replaced the 'split("://")' logic with a regex to match the protocol.
104+
// This correctly identifies schemes like `javascript:` which don't use `//`.
105+
// However, we need to be careful not to confuse authentication credentials (user:password@host)
106+
// with protocols. A colon before an @ symbol might be part of auth, not a protocol separator.
107+
const protocol_match = url.match(/^([a-z][a-z0-9+\-.]*):/i);
108+
let had_explicit_protocol = false;
109+
110+
const cleanUpProtocol = (potential_protocol) => {
111+
had_explicit_protocol = true;
112+
protocol = potential_protocol.toLowerCase();
113+
106114
if (options.require_valid_protocol && options.protocols.indexOf(protocol) === -1) {
115+
// The identified protocol is not in the allowed list.
107116
return false;
108117
}
118+
119+
// Remove the protocol from the URL string.
120+
return url.substring(protocol_match[0].length);
121+
};
122+
123+
if (protocol_match) {
124+
const potential_protocol = protocol_match[1];
125+
const after_colon = url.substring(protocol_match[0].length);
126+
127+
// Check if what follows looks like authentication credentials (user:password@host)
128+
// rather than a protocol. This happens when:
129+
// 1. There's no `//` after the colon (protocols like `http://` have this)
130+
// 2. There's an `@` symbol before any `/`
131+
// 3. The part before `@` contains only valid auth characters (alphanumeric, -, _, ., %, :)
132+
const starts_with_slashes = after_colon.slice(0, 2) === '//';
133+
134+
if (!starts_with_slashes) {
135+
const first_slash_position = after_colon.indexOf('/');
136+
const before_slash = first_slash_position === -1
137+
? after_colon
138+
: after_colon.substring(0, first_slash_position);
139+
const at_position = before_slash.indexOf('@');
140+
141+
if (at_position !== -1) {
142+
const before_at = before_slash.substring(0, at_position);
143+
const valid_auth_regex = /^[a-zA-Z0-9\-_.%:]*$/;
144+
const is_valid_auth = valid_auth_regex.test(before_at);
145+
146+
if (is_valid_auth) {
147+
// This looks like authentication (e.g., user:password@host), not a protocol
148+
if (options.require_protocol) {
149+
return false;
150+
}
151+
152+
// Don't consume the colon; let the auth parsing handle it later
153+
} else {
154+
// This looks like a malicious protocol (e.g., javascript:alert();@host)
155+
url = cleanUpProtocol(potential_protocol);
156+
157+
if (url === false) {
158+
return false;
159+
}
160+
}
161+
} else {
162+
// No @ symbol, this is definitely a protocol
163+
url = cleanUpProtocol(potential_protocol);
164+
165+
if (url === false) {
166+
return false;
167+
}
168+
}
169+
} else {
170+
// Starts with '//', this is definitely a protocol like http://
171+
url = cleanUpProtocol(potential_protocol);
172+
173+
if (url === false) {
174+
return false;
175+
}
176+
}
109177
} else if (options.require_protocol) {
110178
return false;
111-
} else if (url.slice(0, 2) === '//') {
112-
if (!options.allow_protocol_relative_urls) {
179+
}
180+
181+
// Handle leading '//' only as protocol-relative when there was NO explicit protocol.
182+
// If there was an explicit protocol, '//' is the normal separator
183+
// and should be stripped unconditionally.
184+
if (url.slice(0, 2) === '//') {
185+
if (!had_explicit_protocol && !options.allow_protocol_relative_urls) {
113186
return false;
114187
}
115-
split[0] = url.slice(2);
188+
189+
url = url.slice(2);
116190
}
117-
url = split.join('://');
118191

119192
if (url === '') {
120193
return false;

test/validators.test.js

Lines changed: 96 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -424,11 +424,12 @@ describe('Validators', () => {
424424
'http://[2010:836B:4179::836B:4179]',
425425
'http://example.com/example.json#/foo/bar',
426426
'http://1337.com',
427-
428-
'data:text/html,<script>alert(1)</script>',
429-
'file:///etc/passwd',
430-
'blob:https://example.com/uuid',
431-
'vbscript:MsgBox%20Hello',
427+
// TODO: those probably should not be marked as valid URLs; CVE-2025-56200
428+
/* eslint-disable no-script-url */
429+
'javascript:%61%6c%65%72%74%28%31%[email protected]',
430+
'http://[email protected]/',
431+
'javascript:alert(1)@example.com',
432+
/* eslint-enable no-script-url */
432433
],
433434
invalid: [
434435
'http://localhost:3000/',
@@ -470,6 +471,18 @@ describe('Validators', () => {
470471
'////foobar.com',
471472
'http:////foobar.com',
472473
'https://example.com/foo/<script>alert(\'XSS\')</script>/',
474+
// the following tests are because of CVE-2025-56200
475+
/* eslint-disable no-script-url */
476+
"javascript:alert(1);a=';@example.com/alert(1)'",
477+
'JaVaScRiPt:alert(1)@example.com',
478+
'javascript:/* comment */alert(1)@example.com',
479+
'javascript:var a=1; alert(a);@example.com',
480+
'javascript:alert(1)@[email protected]',
481+
'javascript:alert(1)@example.com?q=safe',
482+
'data:text/html,<script>alert(1)</script>@example.com',
483+
'vbscript:msgbox("XSS")@example.com',
484+
'//evil-site.com/[email protected]',
485+
/* eslint-enable no-script-url */
473486
],
474487
});
475488
});
@@ -503,9 +516,11 @@ describe('Validators', () => {
503516
}],
504517
valid: [
505518
'rtmp://foobar.com',
519+
'rtmp:foobar.com',
506520
],
507521
invalid: [
508522
'http://foobar.com',
523+
'tel:+15551234567',
509524
],
510525
});
511526
});
@@ -558,6 +573,9 @@ describe('Validators', () => {
558573
'rtmp://foobar.com',
559574
'http://foobar.com',
560575
'test://foobar.com',
576+
// Dangerous! This allows to mark malicious URLs as a valid URL (CVE-2025-56200)
577+
// eslint-disable-next-line no-script-url
578+
'javascript:alert(1);@example.com',
561579
],
562580
invalid: [
563581
@@ -729,6 +747,61 @@ describe('Validators', () => {
729747
});
730748
});
731749

750+
it('should validate authentication strings if a protocol is not required', () => {
751+
test({
752+
validator: 'isURL',
753+
args: [{
754+
require_protocol: false,
755+
}],
756+
valid: [
757+
758+
],
759+
invalid: [
760+
'user:pw,@foobar.com/',
761+
],
762+
});
763+
});
764+
765+
it('should reject authentication strings if a protocol is required', () => {
766+
test({
767+
validator: 'isURL',
768+
args: [{
769+
require_protocol: true,
770+
}],
771+
valid: [
772+
'http://user:[email protected]/',
773+
'https://user:[email protected]',
774+
'ftp://admin:[email protected]/',
775+
],
776+
invalid: [
777+
778+
779+
780+
],
781+
});
782+
});
783+
784+
it('should reject invalid protocols when require_valid_protocol is enabled', () => {
785+
test({
786+
validator: 'isURL',
787+
args: [{
788+
require_valid_protocol: true,
789+
protocols: ['http', 'https', 'ftp'],
790+
}],
791+
valid: [
792+
'http://example.com',
793+
'https://example.com',
794+
'ftp://example.com',
795+
],
796+
invalid: [
797+
// eslint-disable-next-line no-script-url
798+
'javascript:alert(1);@example.com',
799+
'data:text/html,<script>alert(1)</script>@example.com',
800+
'file:///etc/[email protected]',
801+
],
802+
});
803+
});
804+
732805
it('should let users specify a host whitelist', () => {
733806
test({
734807
validator: 'isURL',
@@ -807,6 +880,24 @@ describe('Validators', () => {
807880
});
808881
});
809882

883+
it('GHSA-9965-vmph-33xx vulnerability - protocol delimiter parsing difference', () => {
884+
const DOMAIN_WHITELIST = ['example.com'];
885+
886+
test({
887+
validator: 'isURL',
888+
args: [{
889+
protocols: ['https'],
890+
host_whitelist: DOMAIN_WHITELIST,
891+
require_host: false,
892+
}],
893+
valid: [],
894+
invalid: [
895+
// eslint-disable-next-line no-script-url
896+
"javascript:alert(1);a=';@example.com/alert(1)",
897+
],
898+
});
899+
});
900+
810901
it('should allow rejecting urls containing authentication information', () => {
811902
test({
812903
validator: 'isURL',

0 commit comments

Comments
 (0)