diff --git a/README.md b/README.md
index 0c9679f2e..5659cbcf8 100644
--- a/README.md
+++ b/README.md
@@ -164,7 +164,7 @@ Validator | Description
**isSurrogatePair(str)** | check if the string contains any surrogate pairs chars.
**isUppercase(str)** | check if the string is uppercase.
**isSlug(str)** | check if the string is of type slug.
-**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 }`
+**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, symbolRegex: /^[-#!$@£€%^&*()_+\|~=\`{}\[\]:";'<>?,.\/\\ ]$/ }``
`symbolRegex` overrides which characters count as symbols. It is tested against one character at a time, so it should be anchored and match a single character (e.g. `/^[!@#]$/`); do not use the `g` or `y` flags, since `RegExp.prototype.test` is stateful with them and will produce inconsistent results. Uppercase letters, lowercase letters and digits are classified before symbols, so a `symbolRegex` that also matches them will not make them count as symbols.
**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-IN', '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` - **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).
diff --git a/src/lib/isStrongPassword.js b/src/lib/isStrongPassword.js
index 8fe9223b7..09ce9b86c 100644
--- a/src/lib/isStrongPassword.js
+++ b/src/lib/isStrongPassword.js
@@ -4,7 +4,6 @@ import assertString from './util/assertString';
const upperCaseRegex = /^[A-Z]$/;
const lowerCaseRegex = /^[a-z]$/;
const numberRegex = /^[0-9]$/;
-const symbolRegex = /^[-#!$@£%^&*()_+|~=`{}\[\]:";'<>?,.\/\\ ]$/;
const defaultOptions = {
minLength: 8,
@@ -19,6 +18,7 @@ const defaultOptions = {
pointsForContainingUpper: 10,
pointsForContainingNumber: 10,
pointsForContainingSymbol: 10,
+ symbolRegex: /^[-#!$@£€%^&*()_+|~=`{}\[\]:";'<>?,.\/\\ ]$/,
};
/* Counts number of occurrences of each char in a string
@@ -38,7 +38,7 @@ function countChars(str) {
}
/* Return information about a password */
-function analyzePassword(password) {
+function analyzePassword(password, symbolRegex) {
let charMap = countChars(password);
let analysis = {
length: password.length,
@@ -84,8 +84,8 @@ function scorePassword(analysis, scoringOptions) {
export default function isStrongPassword(str, options = null) {
assertString(str);
- const analysis = analyzePassword(str);
options = merge(options || {}, defaultOptions);
+ const analysis = analyzePassword(str, options.symbolRegex);
if (options.returnScore) {
return scorePassword(analysis, options);
}
diff --git a/test/sanitizers.test.js b/test/sanitizers.test.js
index e36ba48d3..27c36f15a 100644
--- a/test/sanitizers.test.js
+++ b/test/sanitizers.test.js
@@ -271,6 +271,20 @@ describe('Sanitizers', () => {
});
});
+ it('should score passwords with a custom symbolRegex', () => {
+ test({
+ sanitizer: 'isStrongPassword',
+ args: [{
+ returnScore: true,
+ symbolRegex: /^[!]$/,
+ }],
+ expect: {
+ 'Abc123!': 47, // '!' still matches the custom regex
+ Abc123$: 37, // '$' no longer counts as a symbol
+ },
+ });
+ });
+
it('should score passwords with default options', () => {
test({
sanitizer: 'isStrongPassword',
diff --git a/test/validators.test.js b/test/validators.test.js
index 3d2c8e8b2..e20dcd9d0 100644
--- a/test/validators.test.js
+++ b/test/validators.test.js
@@ -14389,6 +14389,54 @@ describe('Validators', () => {
'PASSWORD!',
],
});
+
+ // narrower symbol set: default symbols outside it stop counting
+ test({
+ validator: 'isStrongPassword',
+ args: [{ symbolRegex: /^[!@#]$/ }],
+ valid: [
+ 'Password1!',
+ 'Passw0rd@#',
+ ],
+ invalid: [
+ 'Password1$',
+ 'Password1€',
+ ],
+ });
+
+ // wider symbol set: characters outside the default class now count
+ test({
+ validator: 'isStrongPassword',
+ args: [{ symbolRegex: /^[§¥]$/ }],
+ valid: [
+ 'Password1§',
+ ],
+ invalid: [
+ 'Password1!',
+ ],
+ });
+
+ // symbolRegex cannot reclassify letters/digits - upper/lower/number are
+ // matched before symbolRegex in analyzePassword's else-if chain
+ test({
+ validator: 'isStrongPassword',
+ args: [{ symbolRegex: /^[a-z0-9]$/ }],
+ invalid: [
+ 'Passwordabc1',
+ ],
+ });
+
+ // custom symbolRegex composes with the other thresholds
+ test({
+ validator: 'isStrongPassword',
+ args: [{ symbolRegex: /^[!]$/, minSymbols: 2, minLength: 6 }],
+ valid: [
+ 'Ab1!c!',
+ ],
+ invalid: [
+ 'Ab1!cd',
+ ],
+ });
});
it('should validate date', () => {