Flask-Middleware / flask-security

Quick and simple security for Flask applications
MIT License
622 stars 154 forks source link

CSRF protection doesn't turn off on token requests #954

Closed sr-verde closed 3 months ago

sr-verde commented 3 months ago

Hey,

I am trying to disable CSRF protection for token-based access. But I can't get it to work. The form doesn't validate because of {'csrf_token': ['The CSRF token is missing.']}. I am not sure if this is a problem with Flask Security or Flask WTF. So I made a minimal example:

import os

from flask import Flask, render_template_string
from flask_wtf import CSRFProtect, FlaskForm
from wtforms.fields import StringField
from flask_sqlalchemy import SQLAlchemy
from flask_security import (
    Security,
    SQLAlchemyUserDatastore,
    auth_required,
    hash_password,
)
from flask_security.models import fsqla_v3 as fsqla

# Create app
app = Flask(__name__)
app.config["DEBUG"] = True

app.config["SECRET_KEY"] = "This is no secret key"
# Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
# Generate a good salt using: secrets.SystemRandom().getrandbits(128)
app.config["SECURITY_PASSWORD_SALT"] = "This is not my salt"

# have session and remember cookie be samesite (flask/flask_login)
app.config["REMEMBER_COOKIE_SAMESITE"] = "strict"
app.config["SESSION_COOKIE_SAMESITE"] = "strict"

# Use an in-memory db
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite://"
# As of Flask-SQLAlchemy 2.4.0 it is easy to pass in options directly to the
# underlying engine. This option makes sure that DB connections from the
# pool are still valid. Important for entire application since
# many DBaaS options automatically close idle connections.
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {
    "pool_pre_ping": True,
}
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False

# Create database connection object
db = SQLAlchemy(app)

# Define models
fsqla.FsModels.set_db_info(db)

class Role(db.Model, fsqla.FsRoleMixin):
    pass

class User(db.Model, fsqla.FsUserMixin):
    pass

# Disable pre-request CSRF
app.config["WTF_CSRF_CHECK_DEFAULT"] = False

# Don't check for CSRF in token and session based requests
app.config["SECURITY_CSRF_PROTECT_MECHANISMS"] = ["basic"]

# Enable CSRF protection
CSRFProtect(app)

# Setup Flask-Security
user_datastore = SQLAlchemyUserDatastore(db, User, Role)
app.security = Security(app, user_datastore)

class MyForm(FlaskForm):
    name = StringField("name")

@app.route("/")
@auth_required("token", "session")
def home_page():
    return render_template_string(
        """
        <html>
        <body>
            Your Auth Token: <pre>{{ current_user.get_auth_token() }}</pre>
            <form action="/" method="post">
                <input type="text" name="name" value="Your name "/>
                <input type="submit" value="OK" />
            </form>
        </body>
        </html>
        """
    )

@app.route("/", methods=["POST"])
@auth_required("token", "session")
def post_page():
    form = MyForm()

    if form.validate_on_submit():
        return render_template_string("Successfully posted.")

    return render_template_string(f"Wasn't successful: {form.errors}")

# one time setup
with app.app_context():
    # Create User to test with
    db.create_all()
    if not app.security.datastore.find_user(email="user@example.com"):
        app.security.datastore.create_user(
            email="user@example.com", password=hash_password("password")
        )
    db.session.commit()

if __name__ == "__main__":
    app.run()

You will get the "CSRF token missing" error for both token and session based auth. But if I understand the documentation correctly, it shouldn't be checked at all. As far as I can see, the CSRF error happens on form.validate_on_submit. I was able to fix this by modifying Flask Security handle_csrf function. I added g.csrf_valid = True for all methods not in CSRF_PROTECT_MECHANISMS and the CSRF token isn't checked anymore (but I’m not sure this is a proper solution for my problem).

Can you help me to understand if this is a problem with Flask Security, Flask WTF, or me?

jwag956 commented 3 months ago

Definitely an issue - the mechanism that Flask Secrurity uses to bypass CSRF in this case is built into our forms - not available to external forms. Let me look at that.