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
Related
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.
Since Django 1.5 raw post data is accessible via request.body.
In my application I sometimes get data send via a form and sometimes raw data (json for example).
Is there any way to write a function like this that does not fail?
def get_post_var(request, name):
result = request.POST.get(name)
if result:
return result
post_body = dict(urlparse.parse_qsl(request.body))
result = post_body.get(name)
if result:
return result
return None
Use request.data instead of request.body.
request.data does not read the data stream again.
The error You cannot access body after reading from request's data stream will be triggered on a request if (1) that request method is POST, (2) that request's POST dictionary is accessed in middleware, in either process_request or process_view and (3) within the view function, request.body is accessed. It is on (3) that the error will be raised, even though the real cause of the bug is (2).
In order to resolve the error, you need to examine your middleware for where it accesses request.POST and modify it such that it doesn't access request.POST anymore.
The Django docs say that middleware should not access request.POST, and this is one consequence of ignoring that recommendation.
Also check out this Django ticket on the issue, which includes the note:
[M]iddleware that hits request.POST should (usually) be considered a
bug. It means that the view will be unable to set any custom upload
handlers, perform custom parsing of the request body, or enforce
permission checks prior to file uploads being accepted.
Adding to Adam Easterling's answer it is worth noting that Django itself 'violates' the hint of not using request.POST in middleware:
The CsrfViewMiddleware class can be considered an exception, as it
provides the csrf_exempt() and csrf_protect() decorators which allow
views to explicitly control at what point the CSRF validation should
occur.
Which does not sanitilize the violation IMO
For those interested to know, I faced this issue:
You cannot access body after reading from request's data stream
when I added 'oauth2_provider.contrib.rest_framework.OAuth2Authentication'
in the "REST_FRAMEWORK" like so in the settings.py:
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
...
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
...
),
Of course disabling this will work but not a workaround I would be proud of.
I was able to read my request.POST after putting #csrf_exempt before my view function. Because CSRF middleware accesses POST data.
I found a trick, using for my middleware,
request._read_started = False
after doing that, ready body again and it works.
For those with the same error who are not readying the body or POST, I had this same error when I used this line of code in a process_view middleware::
event = request.event if 'event' in request else None
Solved by settings request.event = None at the top of the function so I could then use:
event = request.event
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()}}
I have an iPhone application from which I would like to call a post service passing parameters in its request, doing so caused a server error 500.
I have read Django documentation here and I still haven't figure out how to get a csrf_token and how to add it to the AFNetworking AFHTTPRequestOperationManager POST method.
On the server side I've added django.middleware.csrf.CsrfViewMiddleware in the MIDDLEWARE_CLASSES section, but it doesn't seem to do the trick.
My view looks like this; I am not doing much, just hoping to pass.
from django.core.context_processors import csrf
from django.shortcuts import render_to_response
def foo(request):
c={}
c.update(csrf(request))
if request.method == 'POST':
return HttpResponseRedirect("Post received");
The Django CSRF Middleware uses cookies and forms and whatnot to send a code to the page, then make sure the correct page is the one sending information back. In both cases, you must do a GET request to the server, and if you have the middleware installed correctly, it will put the CSRF token into a cookie for you.
Check out the documentation for more info on this.
Now, I noticed you're using a library that uses NSURLConnection, so that should handle cookies for you. I got this bundle of code (untested) that lets you pull the cookie name that you specify in your settings file (again, check out the documentation link above) then put that in your POST.
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL: networkServerAddress];
for (NSHTTPCookie *cookie in cookies)
{
// get the right cookie
}
Of course, if you're only making POSTs and never GETs first, you don't have a CSRF token to send!
And that's why we have the #csrf_exempt tag. (Docs here) This is the way to go 99% of the time, since most apps you won't do a GET before you do a POST. (in webpages you have to do a GET first). Note that this is intended only when an app is sending only POSTs and there's no session to speak of. You really need to think about your own security when using this, and how you verify that a given app/user really is who they claim to be. And how you disable people from hitting this URL from a webbrowser.
TLDR: Probably use #csrf_exempt on the view, but be careful.
I am able to do GET to work with SessionAuthentication and Tastypie without setting any headers except for content-type to application/json. HTTP POST however just fails even though the Cookie in the Header has the session id. It fails with a 401 AuthorizationHeader but it has nothing to do with Authorization. Changing SessionAuthentication to BasicAuthentication and passing username/password works too.
Has anyone ever got SessionAuthentication to work with POST with Tastypie?
Yes I have gotten it to work. All you need to do is to pass the csfr token:
SessionAuthentication
This authentication scheme uses the built-in
Django sessions to check if a user is logged. This is typically useful
when used by Javascript on the same site as the API is hosted on.
It requires that the user has logged in & has an active session. They
also must have a valid CSRF token.
This is how you do that in jQuery:
// sending a csrftoken with every ajax request
function csrfSafeMethod(method) {
// these HTTP methods do not require CSRF protection
return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
}
$.ajaxSetup({
crossDomain: false, // obviates need for sameOrigin test
beforeSend: function(xhr, settings) {
if (!csrfSafeMethod(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", $.cookie('csrftoken'));
}
}
});
$.ajax({
type: "POST",
// ...
Notice the part that says $.cookie('csrftoken'). It gets the csrf token from a cookie that Django sets.
Update:
I had some problems with Django not setting the cookie on Firefox and Opera. Putting the template tag {% csrf_token %} in your template solves this. The right solution would probably be to use the decorator ensure_csrf_cookie().
Here are some additions to Dan's answer. Please, correct me if something is wrong, I am still a bit confused about it myself.
Before we continue, read about CSRF protection in Django. Read it carefully. You need to put the token from the cookie into the header X-CSRFToken. This will not work if the cookie is Httponly, that is, if you have set CSRF-COOKIE-HTTPONLY = True in settings.py. Then you have to embed the cookie in the document which, of course, creates further vulnerabilities and reduces protection the gained by using Httponly.
As far as I can tell, if the cookie is not Httponly, jQuery sets X-CSRFToken automatically. Correct me if I am wrong, but I've just spent several hours playing with it, and this is what I am consistently getting. This makes me wonder, what is the point of the advice in Django documentation? Is it a new feature in jQuery?
Further discussion:
Tastypie disables CSRF protection except with Session Authentication, where it has custom code in authentication.py. You have to pass both the cookie csrftoken cookie and the header X-CSRFToken for the authentication to work. (This is Tastypie's requirement.) Assuming same domain, the browser will pass the cookies. JQuery will pass the header for you unless the csrftoken cookie is Httponly. Conversely, if the cookie is Httponly, I was unable to even manually set the header in $.ajaxSetup{beforeSend.... It appears that jQuery automatically sets X-CSRFToken to null if the csrftoken cookie is Httponly. At least I was able to set the header X-CS_RFToken to what I wanted, so I know I passed the value correctly. I am using jQuery 1.10.
If you are using curl for testing, you have to pass two cookies (sessionid and csrftoken), set the headers X-CSRFToken and, if the protocol is HTTPS, also set the Referrer.
I found this in the tastypie source code. Basically implies that HTTP POST is not supported by SessionAuthentication.
class SessionAuthentication(Authentication):
"""
An authentication mechanism that piggy-backs on Django sessions.
This is useful when the API is talking to Javascript on the same site.
Relies on the user being logged in through the standard Django login
setup.
Requires a valid CSRF token.
"""
def is_authenticated(self, request, **kwargs):
"""
Checks to make sure the user is logged in & has a Django session.
"""
# Cargo-culted from Django 1.3/1.4's ``django/middleware/csrf.py``.
# We can't just use what's there, since the return values will be
# wrong.
# We also can't risk accessing ``request.POST``, which will break with
# the serialized bodies.
if request.method in ('GET', 'HEAD', 'OPTIONS', 'TRACE'):
return request.user.is_authenticated()
So answering my own question here but if someone can explain it better and suggest a good way to do this, that would be great too.
Edit
I am currently using a workaround from https://github.com/amezcua/TastyPie-DjangoCookie-Auth/blob/master/DjangoCookieAuth.py which basically is a custom authentication scheme that fetches the session_id from the cookie and checks with the backend if it is authenticated. Might not be the most full proof solution but works great.