I've run into a problem concerning Flask-WTF's csrf protection.
When a form is instantiated like this:
uform = UserForm(csrf_enabled=False)
everything works as expected, and the form is correctly displayed. However:
uform = UserForm()
results in a TypeError:
Traceback (most recent call last):
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2463, in __call__
return self.wsgi_app(environ, start_response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2449, in wsgi_app
response = self.handle_exception(e)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1866, in handle_exception
reraise(exc_type, exc_value, tb)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/_compat.py", line 39, in reraise
raise value
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2446, in wsgi_app
response = self.full_dispatch_request()
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1952, in full_dispatch_request
return self.finalize_request(rv)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 1969, in finalize_request
response = self.process_response(response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/app.py", line 2268, in process_response
self.session_interface.save_session(self, ctx.session, response)
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/sessions.py", line 387, in save_session
samesite=samesite,
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/werkzeug/wrappers/base_response.py", line 481, in set_cookie
samesite=samesite,
File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/werkzeug/http.py", line 1163, in dump_cookie
buf = [key + b"=" + _cookie_quote(value)]
TypeError: unsupported operand type(s) for +: 'NoneType' and 'bytes'
A SECRET_KEY is set (and so is a WTF_CSRF_SECRET_KEY, just to make sure Flask-WTF didn't expect it, even though according to the docs it's optional).
Here's the rest of the (relevant) code:
admin/routes.py
from flask import current_app as app
from .. import db
from ..models import User
from .forms import UserForm
# Set up a Blueprint
admin_bp = Blueprint('admin_bp', __name__, template_folder='templates', static_folder='static')
@admin_bp.route("users/add/", methods=["GET", "POST"])
def add_user():
#Add user to the DB
add_user = True
#uform = UserForm(csrf_enabled=True)
uform = UserForm()
if uform.validate_on_submit():
user = User(username=uform.username.data, email=uform.email.data, admin=uform.admin.data, address=uform.address.data, delivery=uform.delivery.data)
user.pwhash(uform.password.data)
try:
db.session.add(user)
db.session.commit()
flash("Benutzer '", user.username, "' erfolgreich hinzugef gt.")
except:
flash("Fehler: Benutzer konnte nicht hinzugef gt werden. Existiert Benutzer bereits?")
return redirect(url_for("admin_bp.users"))
return render_template("/user.html", action="Hinzuf gen", add_user=add_user, uform=uform, title="Benutzer hinzuf gen")
admin/forms.py
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField, PasswordField, BooleanField, SelectField
from wtforms.validators import DataRequired, Email
class UserForm(FlaskForm):
username = StringField("Username", validators=[DataRequired()])
email = StringField("Email", validators=[DataRequired(), Email()])
password = StringField("Neues Passwort")
address = StringField("Adresse")
admin = BooleanField("Admin?")
delivery = SelectField("Delivery", choices=[("1", "test1"), ("2", "test2")])
submit = SubmitField("OK")
__init__.py:
from flask_sqlalchemy import SQLAlchemy
from flask_static_compress import FlaskStaticCompress
from flask_bootstrap import Bootstrap
from flask_wtf.csrf import CSRFProtect
# Globally accessible libraries
db = SQLAlchemy()
bs = Bootstrap()
csrf = CSRFProtect()
def create_app():
app = Flask(__name__, instance_relative_config=False, static_folder="static", template_folder="templates")
from config import Config
app.config.from_object('config.DevConfig')
db.init_app(app)
bs.init_app(app)
csrf.init_app(app)
with app.app_context():
from .models import Image, Gallery, ImageGalleryMap, Delivery, ImageDeliveryMap, Blogpost, User, Log
db.create_all()
compress = FlaskStaticCompress(app)
# Register Blueprints
from website.admin.routes import admin_bp
from website.delivery.routes import delivery_bp
from website.landing.routes import landing_bp
from website.public.routes import public_bp
app.register_blueprint(admin_bp, url_prefix='/admin')
app.register_blueprint(delivery_bp, url_prefix='/delivery')
app.register_blueprint(landing_bp, url_prefix='/landing')
app.register_blueprint(public_bp, url_prefix='/')
return app
Does anyone have an idea how to fix this? For (very obvious) reasons, I'm not exactly keen on setting WTF_CSRF_ENABLED=False...
The only way that I can replicate the Traceback that you have given in your example is if app.secret_key
is valid, but app.session_cookie_name
is set to None
.
Please see the below script that I used for testing. Running this script and visiting http://127.0.0.1:5000
raises TypeError: unsupported operand type(s) for +: 'NoneType' and 'bytes'
, which is the exact exception that you have posted above. If the secret key is not set, we get a different error when we try to instantiate the form, before flask tries to serve the request it raises KeyError: 'A secret key is required to use CSRF.'
. Here's the script:
from flask import Flask, render_template_string
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
app = Flask(__name__)
# comment out below and a KeyError is raised, not a TypeError.
# So the fact that the code raises the TypeError from trying to generate
# a cookie means that the secret key is set, the form is instantiating and
# flask is trying to serve the request. So we can rule out this being a
# problem with secret key not set.
app.secret_key = "lskdjflksdj"
# comment out below and the app runs
app.session_cookie_name = None
class MyForm(FlaskForm):
a_str = StringField()
submit = SubmitField()
template = """
<form action="" method="POST">
{{form.a_str()}}
{{form.submit()}}
</form>
"""
@app.route("/", methods=["GET", "POST"])
def route():
# setting csrf_enabled=False makes the problem go away.
form = MyForm(csrf_enabled=True)
return render_template_string(template, form=form)
if __name__ == "__main__":
app.run(debug=True)
So why does setting csrf_enabled=False
on the form stop the error? The token that flask is generating when the error occurs is the csrf token. When we disable csrf checking on the form, that token no longer needs to be generated and we don't run into the error.
The fact that toggling csrf protection on the form has a direct bearing whether the error raises, or not, does make it look like an issue with the secret key configuration but the real question is why is the value of the key
parameter passed into dump_cookie()
NoneType
?
If you follow the Traceback, you'll see that the last call inside the flask library is here: File "/home/charlotte/hochzeit/venv/lib/python3.6/site-packages/flask/sessions.py", line 387, in save_session
. Here's the source:
response.set_cookie(
name,
val,
expires=expires,
httponly=httponly,
domain=domain,
path=path,
secure=secure,
samesite=samesite,
)
You can see for yourself that the first argument passed to set_cookie
above, is passed to the key
parameter in werkzeug.wrappers.base_response.set_cookie
, which itself is passed through to the key
parameter in werkzeug.http.dump_cookie
(which is None
in the error message).
The value of name
in the save_session
snippet above is defined earlier in the function as name = self.get_cookie_name(app)
, and the body of the get_cookie_name
method is simply return app.session_cookie_name
.
The default value for app.session_cookie_name
is "session"
, but somehow that's getting overridden with None
in your configuration.