CSRF Protection and Sessions¶
Cross Site Request Forgery is an attack vector commonly seen in web security. It depends on the fact that HTTP is stateless. And on the fact that it is hard to tell exactly where a request to your web app really comes from.
How CSRF Attacks Work¶
Imagine that you are using your bank’s website to pay your bills. You have authenticated with the site, and so an authentication cookie is present in your browser. That cookie will be sent with any request that is sent to your bank’s site. Even if those requests come to the site by way of links from some other website.
Let’s further imagine that your bank’s website contains a form that allows you to transfer funds from one account to another. You enter the amount you want into a form text input named “amount”. And you select the destination account from a form select input named “destination_account_number”. Such a form might look like this:
<form action="/transfer" method="POST">
Amount: <input type="text" name="amount" value="0.00" />
To Account:
<select name="destination_account_number">
<option value="1234567" selected>Savings Account</option>
<option value="9876543">Checking Account</option>
</select>
</form>
Now, let’s imagine further a malicious person who creates a website with a form that mimics this well-known structure. The form element need not be visible on the malicious web page. It can be submitted via javascript so that a visitor need only load the web page to submit the form. And it can be submitted in a way that leaves the malicious page showing. A visitor to this site would never know that such a submission was made.
<form action="https://yourbank.com/transfer" method="POST" style="display: none;">
<input type="text" name="amount" value="100.00" />
<input type="text name="destination_account_number" value="5647382" />
</form>
Now, most users who visit this malicious site are not logged in to “yourbank.com”. For them, the request will result in an invisible, unreported 403 error. After all, your bank has properly ensured that you must be authenticated to submit POST requests to the transfer page. But you are authenticated and so when you click on a link to the malicious page, $100 is transfered out of your account and into the account identified in the malicious form.
How to Prevent CSRF Attacks¶
CSRF attacks rely on the fact of persistent authentication, in combination with the inability of a site to ensure that the form that was submitted was in fact located in a page served by the bank’s website. To prevent this type of attack, we have to give our site the ability to make such a distinction.
In general, this is accomplished by generating a one-time value called a token when a visitor loads a page with a potentially vulnerable form. This token is rendered as a hidden input, and when the form is submitted, the value is present in the submitted form data. That incoming value is compared against a value stored on the server, and if it checks out then the form submission can proceed.
If it fails to check out, then the submission can be cancelled. The form that was submitted was not the form that was rendered previously, and an attack is underway.
This approach works quite well. Malicious sites are not able to produce the randomly generated values assigned as tokens. They might be able to copy generated by the protected site, but techniques for signing the produced tokens allow the protected site to distinguish between originals and copies that are used later.
But in order to use the technique, the token produced by the protected site must be persisted. The site has to be able to know which token it sent, so that it can compare it with the token received. And it has to do this on a per-visitor basis, since each visitor will receive a unique token.
In order to persist this value, the protected site must store the token on the server and be able to retrieve it when the same visitor makes a new request. We’ve seen how we can use cookies to persist data between requests. But if we were to use a cookie to store this data, we might take the chance that the information is stolen or duplicated. This would render our protection useless.
Sessions¶
Instead, we can persist the token in a data structure held on the server. We can send an identifier for that data structure to the client in a cookie, and when a new request comes, we can retrieve the data structure and find the token so it can be compared.
Pyramid provides us with the ability to persist this data structure. It uses a mechanism called “sessions”. A session is a dictionary-like object that is usually stored on the server. An identifier is sent back to the client in a cookie, and when a new request is made, that same session object is found. We can use this to persist CSRF tokens sent with our forms, and when those forms are submitted, we can compare the incoming value against the value that was stored.
Let’s take a look at how this works in Pyramid.
We begin by configuring our application to use sessions.
Pyramid provides a reference implementation of sessions in the pyramid.sessions
module.
It’s called the SignedCookieSessionFactory
.
It generates a cookie to be sent to the client machine which contains session data.
This cookie is signed by a secret value, and is verified via that signature when returned.
This prevents forging the cookie, but does not prevent reading its contents.
Warning
Under circumstances requiring real security, this is not an acceptable implementation
Let’s go ahead and use this for our application.
We’ll add the configuration to our security.py
module:
# in security.py
from pyramid.session import SignedCookieSessionFactory
def includeme(config):
# previous configuration
session_secret = os.environ.get('SESSION_SECRET', 'itsaseekrit')
session_factory = SignedCookieSessionFactory(session_secret)
config.set_session_factory(session_factory)
Notice that we are still using the OS environment as a mechanism for providing these types of secret values. We read the appropriate value from the environment and use it to sign our session cookies. A fallback value allows us to operate conveniently in development mode. A more careful implementation might fail configuration if that value is not set properly in the environment.
Engaging CSRF Protection¶
Once we have the ability to use a session to persist a CSRF token, we can secure our journal app against this type of attack. If we wanted to, we could manually construct individual views to require a CSRF token:
from pyramid.session import check_csrf_token
def secure_view(request):
check_csrf_token(request)
# do dangerous stuff
If the manual check fails, then an HTTPBadRequest
exception is raised.
Another approach is to configure your entire application using the set_default_csrf_options
method of the configurator class.
This has the advantage of covering all views reached using an unsafe HTTP method.
Let’s add this form to our configuration in security.py
# in security.py
def includeme(config):
# previous configuration
session_secret = os.environ.get('SESSION_SECRET', 'itsaseekrit')
session_factory = SignedCookieSessionFactory(session_secret)
config.set_session_factory(session_factory)
config.set_default_csrf_options(require_csrf=True)
Once this configuration has been made,
any POST
, PUT
, or DELETE
request made to a view in your app will need to have a csrf token present.
If it doesn’t, then the request will raise a 400 Bad Request
response error.
Remember, this protection only works for pages that use unsafe HTTP methods.
It is your responsibility to ensure that your application does not change data based on input from GET
requests.
It is conceiveable that you might wish to exempt certain views from this protection.
Good candidates might be views which may be loaded via POST
but make no changes to data.
In order to do so, you can pass the require_csrf
argument to the view_config
decorator, like so:
@view_config(route_name='safe', renderer='string', require_csrf=False)
def not_protected(request):
return "This view can be posted to without providing a csrf token"
Passing the Token¶
When CSRF protection has been globally engaged,
you will need to add inputs to any form that will be POST
ed to a view in Pyramid.
Interactions with the token should be handled through the session object.
The session object is attached to the request object, and so is available to you in templates:
<form action="" method="POST">
<input type="hidden" name="csrf_token" value="{{ request.session.get_csrf_token() }}" />
<input type="submit" name="submit" value="Do Dangerous Things" />
</form>
The token will be expected to be present in the body of any POST
request with the name “csrf_token”.
Alternatively you can pass it via a header called “X_CSRF_Token”.
This allows for sending the token in AJAX requests:
<script>
var csrfToken = "{{request.session.get_csrf_token()}}";
$.ajax({
type: "POST",
url: "/myview",
headers: { 'X-CSRF-Token': csrfToken }
}).done(function() {
alert("Deleted");
});
</script>
Testing CSRF-Protected Views¶
When testing views, you may need to handle CSRF-protected views a bit differently.
In unit tests, if the view is protected by virtue of global configuration (config.set_default_csrf_options()
),
then calling the view directly in tests will not invoke CSRF token protections.
You’ll be able to test your views without regard for providing the right CSRF token value.
If you have configured your views to manually check CSRF tokens,
you must ensure that the DummyRequest
you have created for the unit test has a session
attribute.
And you must be certain that that session attribute has a csrf_token
value.
And finally, you must ensure that your request has a csrf_token
parameter which matches that in the session.
If you are performing functional tests using the webtest.TestApp
class,
your application will be configured to use both sessions and CSRF.
You’ll be able to test normally, provided that your templates render the token.
Other Uses for Sessions¶
Sessions are useful for more than just CSRF protection.
If your application has some vital state that must be preserved between requests,
you can set that state on a session by treating it like an ordinary Python dict
:
from pyramid.response import Response
def myview(request):
session = request.session
if 'abc' in session:
session['fred'] = 'yes'
session['abc'] = '123'
if 'fred' in session:
return Response('Fred was in the session')
else:
return Response('Fred was not in the session')
Sending Messages¶
Another common use-case is to use the session for sending flash messages to users.
The flash
method of the session object takes a string argument which is a message.
It sets the passed message in a queue stored with the session.
You can call this method in view code to add a message like so:
@view_config(route_name='messenger', renderer='templates/msg.jinja2')
def some_view(request):
request.session.flash('A message for the user')
return {}
An optional second argument allows you to create different queues for different types of messages:
@view_config(route_name='fancy_messenger', renderer='templates/fancy.jinja2')
def another_view(request):
request.session.flase('An important message', 'importantq')
request.session.flash('A fun message', 'funq')
Reading Messages¶
In template code, you can use the seesion’s pop_flash
method to read the messages that are in the flash queue.
If you provide no arguments, it will return a list containing all messages that have been inserted into the queue so far.
You can provide an argument naming a specific queue and all the messages for that queue will be returned.
If you call the method on an empty queue, it will return an empty list.
<div class="messages">
{% for msg in request.session.pop_flash() %}
<div class="message">{{ msg }}</div>
{% endfor %}
</div>
Again, providing a queue name will allow popping the messages from a specific queue:
<div class="messages important">
{% for msg in request.session.pop_flash('importantq') %}
<div class="message">{{ msg }}</div>
{% endfor %}
</div>
As you might expect, the flash queue system for a session also includes the ability to peek at the messages in a queue without removing them.
Wrap Up¶
In today’s lecture we’ve learned about CSRF and Sessions. We learned how a CSRF attack works, and the basic means by which we can mitigate the attack. We’ve learned to use the CSRF protection provided by Pyramid. And we’ve learned to use Sessions in Pyramid in order to support this protection. We’ve also seen a few ways in which sessions can be useful beyond preventing CSRF attacks.
That’s enough for now.