diff --git a/examples/tutorial/flaskr/__init__.py b/examples/tutorial/flaskr/__init__.py index ab96e719..681aa8ee 100644 --- a/examples/tutorial/flaskr/__init__.py +++ b/examples/tutorial/flaskr/__init__.py @@ -1,10 +1,15 @@ import os from flask import Flask +# Import limiter for brute-force protection +from flask_limiter import Limiter +from flask_limiter.util import get_remote_address + def create_app(test_config=None): """Create and configure an instance of the Flask application.""" + app = Flask(__name__, instance_relative_config=True) app.config.from_mapping( # a default secret that should be overridden by instance config @@ -12,7 +17,12 @@ def create_app(test_config=None): # store the database in the instance folder DATABASE=os.path.join(app.instance_path, "flaskr.sqlite"), ) - + # Secure session cookie settings for production safety + app.config.update( + SESSION_COOKIE_SECURE=True, # Cookie sent only over HTTPS + SESSION_COOKIE_HTTPONLY=True, # Prevent JavaScript access (XSS protection) + SESSION_COOKIE_SAMESITE="Lax", # Helps protect against CSRF + ) if test_config is None: # load the instance config, if it exists, when not testing app.config.from_pyfile("config.py", silent=True) @@ -22,6 +32,14 @@ def create_app(test_config=None): # ensure the instance folder exists os.makedirs(app.instance_path, exist_ok=True) + # Initialize rate limiter to prevent brute-force login attacks + limiter = Limiter( + get_remote_address, + app=app, + default_limits=["200 per day", "50 per hour"], # Global safety limits + ) + # Make limiter accessible inside other files (like auth.py) + app.extensions["limiter"] = limiter @app.route("/hello") def hello(): @@ -45,4 +63,4 @@ def create_app(test_config=None): # the tutorial the blog will be the main index app.add_url_rule("/", endpoint="index") - return app + return app \ No newline at end of file diff --git a/examples/tutorial/flaskr/auth.py b/examples/tutorial/flaskr/auth.py index 34c03a20..3169e35d 100644 --- a/examples/tutorial/flaskr/auth.py +++ b/examples/tutorial/flaskr/auth.py @@ -1,5 +1,5 @@ import functools - +import re from flask import Blueprint from flask import flash from flask import g @@ -8,10 +8,17 @@ from flask import render_template from flask import request from flask import session from flask import url_for -from werkzeug.security import check_password_hash -from werkzeug.security import generate_password_hash +# from werkzeug.security import check_password_hash +# from werkzeug.security import generate_password_hash from .db import get_db +# Modern secure password hashing (Argon2) +from argon2 import PasswordHasher +from argon2.exceptions import VerifyMismatchError + +#Create Argon2 password hasher instance +ph = PasswordHasher() + bp = Blueprint("auth", __name__, url_prefix="/auth") @@ -58,14 +65,26 @@ def register(): if not username: error = "Username is required." + # Strong password policy enforcement + elif len(password) < 8: + error = "Password must be at least 8 characters." + elif not re.search(r"[A-Z]", password): + error = "Password must contain an uppercase letter." + elif not re.search(r"[a-z]", password): + error = "Password must contain a lowercase letter." + elif not re.search(r"[0-9]", password): + error = "Password must contain a number." + elif not re.search(r"[!@#$%^&*]", password): + error = "Password must contain a special character." elif not password: error = "Password is required." if error is None: try: + db.execute( "INSERT INTO user (username, password) VALUES (?, ?)", - (username, generate_password_hash(password)), + (username, ph.hash(password)), ) db.commit() except db.IntegrityError: @@ -82,8 +101,10 @@ def register(): @bp.route("/login", methods=("GET", "POST")) +# Prevent brute-force attacks by limiting login attempts def login(): """Log in a registered user by adding the user id to the session.""" + """Authenticate user using Argon2 password verification.""" if request.method == "POST": username = request.form["username"] password = request.form["password"] @@ -95,8 +116,12 @@ def login(): if user is None: error = "Incorrect username." - elif not check_password_hash(user["password"], password): - error = "Incorrect password." + else: + try: + #Secure Argon2 password verification + ph.verify(user["password"], password) + except VerifyMismatchError: + error = "Incorrect password." if error is None: # store the user id in a new session and return to the index @@ -113,4 +138,4 @@ def login(): def logout(): """Clear the current session, including the stored user id.""" session.clear() - return redirect(url_for("index")) + return redirect(url_for("index")) \ No newline at end of file