Flask-WTF: CSRF token missing - python

What seemed like a simple bug - a form submission that won't go through due to a "CSRF token missing" error - has turned into a day of hair pulling. I have gone through every SO article related to Flask or Flask-WTF and missing CSRF tokens, and nothing seems to be helping.
Here are the details:
Following Martijin's guidelines to an earlier question:
The Flask-WTF CSRF infrastructure rejects a token if:
1) the token is missing. Not the case here, you can see the token in the form.
The token is definitely present in my form, and being POST'ed successfully
2) it is too old (default expiration is set to 3600 seconds, or an hour).
Set the TIME_LIMIT attribute on forms to override this. Probably not the
case here.
Also OK for me - the token is well within the default expiration time
3) if no 'csrf_token' key is found in the current session. You can
apparently see the session token, so that's out too.
In my case, session['csrf_token'] is properly set and seen by Flask
4) If the HMAC signature doesn't match; the signature is based on the
random value set in the session under the 'csrf_token' key, the
server-side secret, and the expiry timestamp in the token.
This is my problem. The HMAC comparison between the submitted form's CSRF and the session CSRF fails. And yet I don't know how to solve it. I've been desperate enough (as with the other questioner) to dig into Flask-WTF code and set debugging messages to find out what's going on. As best I can tell, it's working like this:
1) generate_csrf_token() in "form.py" (Flask-WTF) wants to generates a CSRF token. So it calls:
2) generate_csrf() in "csrf.py". That function generates a new session['csrf_token'] if one does not exist. In my case, this always happens - although other session variables appear to persist between requests, my debugging shows that I never have a 'csrf_token' in my session at the start of a request. Is this normal?
3) The generated token is returned and presumably incorporated into the form variable when I render hidden fields on the template. (again, debugging shows that this token is present in the form and properly submitted and received)
4) Next, the form is submitted.
5) Now, validate_csrf in csrf.py is called. But since another request has taken place, and generate_csrf() has generated a new session CSRF token, the two timestamps for the two tokens (in session and from the form) will not match. And since the CSRF is made up in part by expiration dates, therefore validation fails.
I suspect the problem is in step #2, where a new token is being generated for every request. But I have no clue why other variables in my session are persisting from request to request, but not "csrf_token".
There is no weirdness going on with SECRET_KEY or WTF_CSRF_SECRET_KEY either (they are properly set).
Anyone have any ideas?

I figured it out. It appears to be a cookie/session limit (which probably beyond Flask's control) and a silent discarding of session variables when the limit is hit (which seems more like a bug).
Here's an example:
templates/hello.html
<p>{{ message|safe }}</p>
<form name="loginform" method="POST">
{{ form.hidden_tag() }}
{{ form.submit_button() }}
</form>
myapp.py
from flask import Flask, make_response, render_template, session
from flask_restful import Resource, Api
from flask_wtf import csrf, Form
from wtforms import SubmitField
app = Flask(__name__)
app.secret_key = '5accdb11b2c10a78d7c92c5fa102ea77fcd50c2058b00f6e'
api = Api(app)
num_elements_to_generate = 500
class HelloForm(Form):
submit_button = SubmitField('Submit This Form')
class Hello(Resource):
def check_session(self):
if session.get('big'):
message = "session['big'] contains {} elements<br>".format(len(session['big']))
else:
message = "There is no session['big'] set<br>"
message += "session['secret'] is {}<br>".format(session.get('secret'))
message += "session['csrf_token'] is {}<br>".format(session.get('csrf_token'))
return message
def get(self):
myform = HelloForm()
session['big'] = list(range(num_elements_to_generate))
session['secret'] = "A secret phrase!"
csrf.generate_csrf()
message = self.check_session()
return make_response(render_template("hello.html", message=message, form=myform), 200, {'Content-Type': 'text/html'})
def post(self):
csrf.generate_csrf()
message = self.check_session()
return make_response("<p>This is the POST result page</p>" + message, 200, {'Content-Type': 'text/html'})
api.add_resource(Hello, '/')
if __name__ == '__main__':
app.run(debug=True)
Run this with num_elements_to_generate set to 500 and you'll get something like this:
session['big'] contains 500 elements
session['secret'] is 'A secret phrase!'
session['csrf_token'] is a6acb57eb6e62876a9b1e808aa1302d40b44b945
and a "Submit This Form" button. Click the button, and you'll get:
This is the POST result page
session['big'] contains 500 elements
session['secret'] is 'A secret phrase!'
session['csrf_token'] is a6acb57eb6e62876a9b1e808aa1302d40b44b945
All well and good. But now change num_elements_to_generate to 3000, clear your cookies, rerun the app and access the page. You'll get something like:
session['big'] contains 3000 elements
session['secret'] is 'A secret phrase!'
session['csrf_token'] is 709b239857fd68a4649deb864868897f0dc0a8fd
and a "Submit This Form" button. Click the button, and this time you'll get:
This is the POST result page
There is no session['big'] set
session['secret'] is 'None'
session['csrf_token'] is 13553dce0fbe938cc958a3653b85f98722525465
3,000 digits stored in the session variable is too much, so the session variables do not persist between requests. Interestingly they DO exist in the session on the first page (no matter how many elements you generate), but they will not survive to the next request. And Flask-WTF, since it does not see a csrf_token in the session when the form is posted, generates a new one. If this was a form validation step, the CSRF validation would fail.
This seems to be a known Flask (or Werkzeug) bug, with a pull request here. I'm not sure why Flask isn't generating a warning here - unless it is somehow technically unfeasible, it's an unexpected and unpleasant surprise that it is silently failing to keep the session variables when the cookie is too big.

Instead of going through the long process mentioned above just add the following jinja code {{ form.csrf_token }} to the html side of the form and that should take care of the "CSRF token missing" error. So on the HTML side it would look something like this:
<form action="{{url_for('signup')}}" method="POST">
{{ form.csrf_token }}
<fieldset class="name">
{{ form.name.label}}
{{ form.name(placeholder='John Doe')}}
</fieldset>
.
.
.
{{ form.submit()}}

Related

How can I use Flask WTForms and CSRF without session cookie?

I have a very simple app that has no user management or any Flask-Login auth needs. It has forms, WTForms. All I want to do is collect some data submitted by the form. I could technically disable CSRF validation but Flask WTForms really urges me not to.
I'd like to disable flask session cookie in the browser because it seems unnecessary and I would need to put a cookie banner for GDPR compliance. So to avoid all that, I thought of disabling flask session cookie as follows:
class CustomSessionInterface(SecureCookieSessionInterface):
""" Disable session cookies """
def should_set_cookie(self, app: "Flask", session: SessionMixin) -> bool:
return False
# App initialization
app = Flask(__name__)
app.session_interface = CustomSessionInterface()
But doing so leads to a 500 error: "The CSRF session token is missing". However, looking at the HTML that was rendered has the following csrf token rendered properly:
<input id="csrf_token" name="csrf_token" type="hidden" value="ImI2ZDIwMDUxMDNmOGM3ZDFlMTI4ZTIzODE4ODBmNDUwNWU3ZmMzM2Ui.YhA2kQ.UnIHwlR1qLL61N9_30lDKngxLlM">
Questions:
What is the relationship between CSRF token validation and session cookie? Why is a cookie necessary to validated the CSRF token?
I tried enabling session cookies again, deleting the cookie in Chrome developer tools leads to the same error. So, indeed, session cookie seems to be absolutely necessary to validate CSRF token.
How can I use CSRF form validation without a session cookie?
Thank you so much.
I found out from the code base of WTForms: https://github.com/wtforms/flask-wtf/blob/565a63d9b33bf6eb141839f03f0032c03894d866/src/flask_wtf/csrf.py#L56
Basically, session['csrf_token'] is stored in the session and compared against the form.hidden() tag (or form.csrf_token) in the HTML body.
This is not clearly explained in the docs. But the codebase makes it clear. I guess there is no way to do CSRF protection without secure cookies.
The downside of this is that you can't get rid of cookies. I suspect, one could build a server-side session database, but then there are issues with scaling your Flask app horizontally.
You should check if it helps you:
https://wtforms.readthedocs.io/en/3.0.x/csrf/#creating-your-own-csrf-implementation
This allows you to create your own implementation of csrf token generation and validation in which you do not need to use cookies.

Using GET in a Django Form

I have a question regarding Django Forms and GET
I have a form to download student scores in CSV format. The fields are name and year so I have a forms.py
StudentDownloadForm(forms.Form):
name=forms.CharField()
year = forms.CharField()
And I want to use this form in the template.html with
context={'student_form' : StudentDownloadForm(),}
<form action ="" method="GET">
{% csrf_token %}{{ student_form|crispy }}
<input type="submit" value="Query"/>
</form>
So my questions are as follows:
If I use the method="GET" then the csrf token is visible in the URL, which is a security issue
Can I then use the method="POST" instead?
Alternatively, can I remove the csrf token in the form?
According to Django documentation (Cross Site Request Forgery protection):
For all incoming requests that are not using HTTP GET, HEAD, OPTIONS
or TRACE, a CSRF cookie must be present, and the ‘csrfmiddlewaretoken’
field must be present and correct. If it isn’t, the user will get a
403 error.
And:
It deliberately ignores GET requests (and other requests that are
defined as ‘safe’ by RFC 2616). These requests ought never to have any
potentially dangerous side effects , and so a CSRF attack with a GET
request ought to be harmless. RFC 2616 defines POST, PUT and DELETE as
‘unsafe’, and all other methods are assumed to be unsafe, for maximum
protection.
So, you can omit CSRF token for GET requiests

Issues sending and receiving a GET request using Flask

I'm having issues with correctly sending and receiving a variable with a GET request. I cannot find any information online either. From the HTML form below, you can see I'm sending the value of 'question' but I'm also receiving 'topic' from a radio button in the form (though the code is for that is not below).
I want to send 'topic' using POST but use GET for 'question'. I'm aware that the form method is POST though I'm not sure how to cater for both POST and GET.
HTML Form:
<form method="POST" action="{{ url_for('topic', question=1) }}">
My second issue is that I'm unsure how to receive 'topic' AND 'question' from the form. I've managed to receive 'topic' as seen below but I'm not quite sure how to receive 'question'. Preferably it would be better for the URL to be like so:
www.website.com/topic/SomeTopic?question=1
For the code below, I found online that request.args[] is used for receiving GET requests though I'm not sure if it is correct.
Flask:
#app.route('/topic/<topic>', methods=['POST', 'GET'])
def questions(topic):
question = request.args['questions']
return render_template('page.html')
The question is
How do I send two variables from a form using GET and POST for different variables at the same time.
How would I go about receiving both variables?
The short answer to your question is that you can't send both GET and POST using the same form.
But if you want your url to look like you specified:
www.website.com/topic/SomeTopic?question=1
then you're almost there. First you will need to already know the name of the topic as you have to specify that in your call to url_for() for the questions url.
<form method="GET" action="{{ url_for('questions', topic_name="cars") }}">
# Your url will be generated as www.website.com/topic/cars
flask
# Note that I changed the variable name here so you can see how
# its related to what's passed into url_for
#app.route('/topic/<topic_name>')
def questions(topic_name):
question = request.args['question']
return render_template('page.html')
Now when you submit your form, your input will be sent as a GET, an asumming you have an input field with the name question you'll be able to get the value of that field.

How to check if request.GET or request.POST is from a particular part of a URL?

Technologies: Django 1.7.7, Python 3
currentUrl = request.get_full_path()
if request.method == 'POST' and request.POST('/path/to/thing', currentUrl):
# Do something
I'm trying to check if the request method is a POST and if the request is coming from a particular URL, but not including the users input as as part of the url (if that makes sense).
So, for example, let's say a user is filling out a form and he/she submits the word "hello." I want to check to see if the form is from "some/base/url" without checking to see if the word "hello" is in the url too.
What is the best way to accomplish this?
Found this to work:
if request.method == 'GET' and '/path/to/thing' in request.path_info
HttpRequest.POST
A dictionary-like object containing all given HTTP POST parameters,
providing that the request contains form data
HttpRequest.POST is not a method to use.
In case you have multiple options to do with your form for example you want to have a submit button, and a submit button with add another then you will need to give a name for each button, and then check wither the name in the POST parameters or not.
In case you are afraid of having the form submitted from unknown place. then csrf_token do it for you.
The CSRF middleware and template tag provides easy-to-use protection
against Cross Site Request Forgeries. This type of attack occurs when
a malicious Web site contains a link, a form button or some javascript
that is intended to perform some action on your Web site, using the
credentials of a logged-in user who visits the malicious site in their
browser. A related type of attack, ‘login CSRF’, where an attacking
site tricks a user’s browser into logging into a site with someone
else’s credentials, is also covered.
Read:
https://docs.djangoproject.com/en/1.8/ref/request-response/
https://docs.djangoproject.com/en/1.8/ref/csrf/

Testing csrf token in django

I'm looking to test if csrf tokens are working in my django site. The issue is that csrf_token returns a token value rather that the custom value of 'csrftoken'. Is there a way to set the value of the csrf for testing? This is the code that I am working with:
token = 'csrftoken'
client = Client(enforce_csrf_checks=True)
client.login(username='user', password='pass')
client.get("/my/web/page/")
csrf_token = client.cookies[token].value
assetEqual(token, csrf_token)
Is there a particular reason you're testing something that Django's own tests already cover in a fuller way?
Or, put another way, is there something specific/non-standard that you're doing with the CSRF token that means you need to test it?
If you're just using it as per the docs, save yourself some time and put the effort into testing your own code, not Django's

Categories