Beta6 of my password flask-wtf web form

hi all

so im pretty much done with making my flask-wtf password web form, may add other bits to it but what you think

from flask import Flask, render_template, request
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField, BooleanField, DecimalField, RadioField, SelectField, TextAreaField, FileField, SubmitField
from wtforms.validators import InputRequired, Length, DataRequired, EqualTo, Regexp, ValidationError
import re
import subprocess
import smtplib
from email.mime.text import MIMEText

app = Flask(__name__)
app.config['SECRET_KEY'] = 'secretkey'

class PasswordForm(FlaskForm):
    un = StringField('Username', [InputRequired(message='please enter your Username')])
    op = PasswordField('Current Password', [InputRequired(message='please enter your current password')])
    np = PasswordField('New Password', [InputRequired(message='please enter your new password')])
    cnp = PasswordField('Confirm New Password')
    dom = SelectField('Domain', choices=[('lon-prod-dc01.domain1.com', 'prod'), ('lon-corp-dc01.domain2.com', 'corp'), ('dc01.robo84.net', 'robo84')])
    
    def validate_un(form, field):
        if not field.data == form.un.data.lower():
            raise ValidationError('Username needs to be Lowercase')
    
    def validate_np(form, field):
        if form.un.data:
            if any (name in field.data.lower() for name in form.un.data.split(".")):
                raise ValidationError('New password cant contain firstname or lastname')
        if field.data.lower() == form.op.data.lower():
            raise ValidationError('New password cant match Current password')
        if re.search(r"\s", field.data):
            raise ValidationError('New password cant contain any spaces')
        if not re.search(r"[0-9]", field.data):
            raise ValidationError('New password has to contain one number')
        if not re.search(r"[a-z]", field.data):
            raise ValidationError('New password has to contain one lower case character')
        if not re.search(r"[A-Z]", field.data):
            raise ValidationError('New password has to contain one upper case character')
        if not re.search(r"[\`\¬\!\"\£\$\%\^\&\*\(\)\-\_\=\+\\\|\[\]\;\'\#\,\.\/\{\}\:\@\~\<\>\?]", field.data):
            raise ValidationError('New password has to contain one special character')
        if len(field.data) < 12:
            raise ValidationError('New password must be at least 12 characters')
        if not field.data == form.cnp.data:
            raise ValidationError('New password has to match Confirm new password')
        
@app.route('/password', methods=['GET', 'POST'])
def password():
    form = PasswordForm()
    if request.method == 'POST' and form.validate():
        result = subprocess.run(f'powershell.exe $cred = Import-CliXml -Path C:\\python\\cred.xml; Set-ADAccountPassword -Credential $cred -Identity {form.un.data} -OldPassword (ConvertTo-SecureString -AsPlainText {form.op.data} -Force) -NewPassword (ConvertTo-SecureString -AsPlainText {form.cnp.data} -Force) -Server {form.dom.data}', capture_output=True, text=True, shell=False)
        if 'The specified network password is not correct' in result.stderr:
            return 'your current password is wrong, please click back and try again'
        if 'The password does not meet the length, complexity, or history requirement of the domain' in result.stderr:
            return 'cant re-use one of your old passwords or cant change password as still less than one day old, please click back and try again'
        if result.returncode == 0:
            try:
                une = subprocess.run(f'powershell.exe $cred = Import-CliXml -Path C:\\python\\cred.xml; Get-aduser -credential $cred -server {form.dom.data} -identity {form.un.data} -properties * | select-object -expandproperty mail', capture_output=True, text=True, shell=False)
                smtp_server = '172.17.1.5'
                smtp_user = 'robertkwild@gmail.com'
                smtp_connection = smtplib.SMTP(smtp_server)
                email_body = f'hello {form.un.data} your new password is {form.cnp.data} for {form.dom.data}'
                email_message = MIMEText(email_body)
                email_message['Subject'] = 'password change'
                email_message['From'] = 'robertkwild@gmail.com'
                smtp_connection.sendmail(smtp_user, {une.stdout}, email_message.as_string())
                smtp_connection.quit()
                return 'found an email for your user, please check email for your new password'
            except:
                return 'changed your password but could not find an email address for you, please contact IT to add one'
    return render_template('password.html', form=form)

if __name__ == '__main__':
    app.run(debug=True, ssl_context='adhoc')
    
#'<h1>The username is {}. The old password is {}. the new password is {}. changing for domain {}'.format(form.un.data, form.op.data, form.cnp.data, form.dom.data)

thanks,
rob

is it easy to add a re capcha to the form even if its hosted internally not public

and maybe an eye icon so you can see the password

Each of your enforcement rules make it easier to brute force the password. An attacker will thank you, as has been explained to you by others already.

2 Likes

how could i avoid this please, sorry

i mean this is only going to be on the LAN not public facing

I already gave you a suggestion to fix that. Remove ALL of your criteria except for the minimum length (and of course that the two passwords match). The others are superfluous and serve only to worsen security (unless you consider that Post-It notes are secure).

I suggest you research ways to calculate password strength.
You could then disallow passwords that were “too weak”.
But I have no experience with how to do such a calculation.

Min password length gets you a long way.

It basically gets you the whole way, actually. If we VERY generously assume that passwords are being generated entirely at random (true for certain password managers, but very definitely false for passwords a user invents for themselves), a 12-character password from a character set of “lower case, upper case, special” (with the OP’s list of 34 valid specials) has, give or take, 96**12 possibilities or 6e23 - a little higher than Avogadro’s Number. In reality, those passwords are going to be FAR less secure than that, but we’re being generous here.

In order to get the same number of possible passwords from nothing but lower-case letters, all you have to do is lengthen it. How much? Not much, actually. A 17-character password drawn from just 26 options gives you 26**17 or 1e24 passwords.

And if you use the XKCD 936 pattern of picking random words (note, they have to be random, don’t just think up a phrase), using a pool of 2000 words, you need just seven such words to have roughly the same strength password (1e23).

But remember, NONE OF THIS MATTERS if the users’ passwords are all written on Post-It notes. So, far more important than any concept of “password strength” is whether the password rules prevent people from memorizing their passwords.

1 Like

If the users are in an office setting where janitorial staff have regular access, passwords on Post-It notes are a serious issue.

If the users are individuals working from home who live by themselves and aren’t tech savvy enough to figure out a password manager, a Post-It note might be the only thing keeping them from using “password1” for the password.