TryGhost/Ghost

View on GitHub
ghost/admin/app/validators/mixins/password.js

Summary

Maintainability
A
3 hrs
Test Coverage
import Mixin from '@ember/object/mixin';
import validator from 'validator';

const BAD_PASSWORDS = [
    '1234567890',
    'qwertyuiop',
    'qwertzuiop',
    'asdfghjkl;',
    'abcdefghij',
    '0987654321',
    '1q2w3e4r5t',
    '12345asdfg'
];
const DISALLOWED_PASSWORDS = ['ghost', 'password', 'passw0rd'];

export default Mixin.create({
    /**
    * Counts repeated characters if 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}
    */
    _characterOccurance(stringToTest) {
        let chars = {};
        let allowedOccurancy;
        let valid = true;

        allowedOccurancy = stringToTest.length / 2;

        // Loop through string and accumulate character counts
        for (let i = 0; i < stringToTest.length; i += 1) {
            if (!chars[stringToTest[i]]) {
                chars[stringToTest[i]] = 1;
            } else {
                chars[stringToTest[i]] += 1;
            }
        }

        // check if any of the accumulated chars exceed the allowed occurancy
        // of 50% of the words' length.
        for (let charCount in chars) {
            if (chars[charCount] >= allowedOccurancy) {
                valid = false;
                return valid;
            }
        }

        return valid;
    },

    passwordValidation(model, password, errorTarget) {
        let blogUrl = model.config?.blogUrl || window.location.host;
        let blogTitle = model.blogTitle || model.config?.blogTitle;
        let blogUrlWithSlash;

        // the password that needs to be validated can differ from the password in the
        // passed model, e. g. for password changes or reset.
        password = password || model.password;
        errorTarget = errorTarget || 'password';

        blogUrl = blogUrl.replace(/^http(s?):\/\//, '');
        blogUrlWithSlash = blogUrl.match(/\/$/) ? blogUrl : `${blogUrl}/`;

        blogTitle = blogTitle ? blogTitle.trim().toLowerCase() : blogTitle;

        // password must be longer than 10 characters
        if (!validator.isLength(password || '', 10)) {
            model.errors.add(errorTarget, 'Password must be at least 10 characters long.');
            return this.invalidate();
        }

        password = password.toString();

        // dissallow password from badPasswords list (e. g. '1234567890')
        BAD_PASSWORDS.forEach((badPassword) => {
            if (badPassword === password) {
                model.errors.add(errorTarget, 'Sorry, you cannot use an insecure password.');
                this.invalidate();
            }
        });

        // password must not match with users' email
        if (password.toLowerCase() === model.email.toLowerCase()) {
            model.errors.add(errorTarget, 'Sorry, you cannot use an insecure password.');
            this.invalidate();
        }

        // password must not contain the words 'ghost', 'password', or 'passw0rd'
        DISALLOWED_PASSWORDS.forEach((disallowedPassword) => {
            if (password.toLowerCase().indexOf(disallowedPassword) >= 0) {
                model.errors.add(errorTarget, 'Sorry, you cannot use an insecure password.');
                this.invalidate();
            }
        });

        // password must not match with blog title
        if (password.toLowerCase() === blogTitle) {
            model.errors.add(errorTarget, 'Sorry, you cannot use an insecure password.');
            this.invalidate();
        }

        // password must not match with blog URL (without protocol, with or without trailing slash)
        if (password.toLowerCase() === blogUrl || password.toLowerCase() === blogUrlWithSlash) {
            model.errors.add(errorTarget, 'Sorry, you cannot use an insecure password.');
            this.invalidate();
        }

        // dissallow passwords where 50% or more of characters are the same
        if (!this._characterOccurance(password)) {
            model.errors.add(errorTarget, 'Sorry, you cannot use an insecure password.');
            this.invalidate();
        }
    }
});