2021-06-14 19:53:24 +03:00
|
|
|
const _ = require('lodash');
|
|
|
|
|
2021-06-15 17:32:36 +03:00
|
|
|
const validator = require('../data/validator');
|
2021-06-14 19:53:24 +03:00
|
|
|
|
2021-06-15 13:20:55 +03:00
|
|
|
const tpl = require('@tryghost/tpl');
|
2021-06-15 14:33:14 +03:00
|
|
|
const settingsCache = require('../services/settings/cache');
|
|
|
|
const urlUtils = require('../../shared/url-utils');
|
2021-06-14 19:53:24 +03:00
|
|
|
|
2021-06-15 13:20:55 +03:00
|
|
|
const messages = {
|
|
|
|
passwordDoesNotComplyLength: 'Your password must be at least {minLength} characters long.',
|
|
|
|
passwordDoesNotComplySecurity: 'Sorry, you cannot use an insecure password.'
|
|
|
|
};
|
|
|
|
|
2021-06-14 19:53:24 +03:00
|
|
|
/**
|
|
|
|
* Counts repeated characters in a string. When 50% or more characters are the same,
|
|
|
|
* we return false and therefore invalidate the string.
|
|
|
|
* @param {String} stringToTest The password string to check.
|
|
|
|
* @return {Boolean}
|
|
|
|
*/
|
|
|
|
function characterOccurance(stringToTest) {
|
|
|
|
const chars = {};
|
|
|
|
let allowedOccurancy;
|
|
|
|
let valid = true;
|
|
|
|
|
|
|
|
stringToTest = _.toString(stringToTest);
|
|
|
|
allowedOccurancy = stringToTest.length / 2;
|
|
|
|
|
|
|
|
// Loop through string and accumulate character counts
|
|
|
|
_.each(stringToTest, function (char) {
|
|
|
|
if (!chars[char]) {
|
|
|
|
chars[char] = 1;
|
|
|
|
} else {
|
|
|
|
chars[char] += 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// check if any of the accumulated chars exceed the allowed occurancy
|
|
|
|
// of 50% of the words' length.
|
|
|
|
_.forIn(chars, function (charCount) {
|
|
|
|
if (charCount >= allowedOccurancy) {
|
|
|
|
valid = false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return valid;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Validation against simple password rules
|
|
|
|
* Returns false when validation fails and true for a valid password
|
|
|
|
* @param {String} password The password string to check.
|
|
|
|
* @param {String} email The users email address to validate agains password.
|
2021-06-15 14:33:14 +03:00
|
|
|
* @param {String} [blogTitle] Optional blogTitle value, when blog title is not set yet, e. g. in setup process.
|
2021-06-14 19:53:24 +03:00
|
|
|
* @return {Object} example for returned validation Object:
|
|
|
|
* invalid password: `validationResult: {isValid: false, message: 'Sorry, you cannot use an insecure password.'}`
|
|
|
|
* valid password: `validationResult: {isValid: true}`
|
|
|
|
*/
|
|
|
|
function validatePassword(password, email, blogTitle) {
|
|
|
|
const validationResult = {isValid: true};
|
|
|
|
const disallowedPasswords = ['password', 'ghost', 'passw0rd'];
|
|
|
|
let blogUrl = urlUtils.urlFor('home', true);
|
|
|
|
|
|
|
|
const badPasswords = [
|
|
|
|
'1234567890',
|
|
|
|
'qwertyuiop',
|
|
|
|
'qwertzuiop',
|
|
|
|
'asdfghjkl;',
|
|
|
|
'abcdefghij',
|
|
|
|
'0987654321',
|
|
|
|
'1q2w3e4r5t',
|
|
|
|
'12345asdfg'
|
|
|
|
];
|
|
|
|
|
|
|
|
blogTitle = blogTitle ? blogTitle : settingsCache.get('title');
|
|
|
|
blogUrl = blogUrl.replace(/^http(s?):\/\//, '');
|
|
|
|
|
|
|
|
// password must be longer than 10 characters
|
|
|
|
if (!validator.isLength(password, 10)) {
|
|
|
|
validationResult.isValid = false;
|
2021-06-15 13:20:55 +03:00
|
|
|
validationResult.message = tpl(messages.passwordDoesNotComplyLength, {minLength: 10});
|
2021-06-14 19:53:24 +03:00
|
|
|
|
|
|
|
return validationResult;
|
|
|
|
}
|
|
|
|
|
|
|
|
// dissallow password from badPasswords list (e. g. '1234567890')
|
|
|
|
_.each(badPasswords, function (badPassword) {
|
|
|
|
if (badPassword === password) {
|
|
|
|
validationResult.isValid = false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// password must not match with users' email
|
|
|
|
if (email && email.toLowerCase() === password.toLowerCase()) {
|
|
|
|
validationResult.isValid = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// password must not contain the words 'ghost', 'password', or 'passw0rd'
|
|
|
|
_.each(disallowedPasswords, function (disallowedPassword) {
|
|
|
|
if (password.toLowerCase().indexOf(disallowedPassword) >= 0) {
|
|
|
|
validationResult.isValid = false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// password must not match with blog title
|
|
|
|
if (blogTitle && blogTitle.toLowerCase() === password.toLowerCase()) {
|
|
|
|
validationResult.isValid = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// password must not match with blog URL (without protocol, with or without trailing slash)
|
|
|
|
if (blogUrl && (blogUrl.toLowerCase() === password.toLowerCase() || blogUrl.toLowerCase().replace(/\/$/, '') === password.toLowerCase())) {
|
|
|
|
validationResult.isValid = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// dissallow passwords where 50% or more of characters are the same
|
|
|
|
if (!characterOccurance(password)) {
|
|
|
|
validationResult.isValid = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Generic error message for the rules where no dedicated error massage is set
|
|
|
|
if (!validationResult.isValid && !validationResult.message) {
|
2021-06-15 13:20:55 +03:00
|
|
|
validationResult.message = tpl(messages.passwordDoesNotComplySecurity);
|
2021-06-14 19:53:24 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return validationResult;
|
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = validatePassword;
|