Queries about CSRF design following penetration test

709 views
Skip to first unread message

David Winterbottom

unread,
Dec 1, 2011, 4:19:27 PM12/1/11
to django-d...@googlegroups.com
All,

A site I work on was penetration tested this week and several queries were raised about the site's (and hence Django's) CSRF implementation.  The points seem valid to a degree but I wanted to check if there were design decisions behind the current implementation.

Note, we're using Django 1.3.1.

CSRF tokens are not generated per-request or with a max age
Django's CSRF token is only generated when the cookie is not found, and as the cookie is set to a max age of a year, the token remains the same between requests (and visits).  Is there a reason why a new token isn't generated for each request?  I appreciate that this doesn't really open up a huge security hole, but it does differ from OWASP's recommendations: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)

The security company also recommend that the CSRF token should have a maximum age so that it expires if you wait too long.

No server-side check of the CSRF token
Basically, as long as the cookie token matches the form token, the request is valid - even if they differ from what was set originally.  I found some previous discussion of this (http://groups.google.com/group/django-developers/browse_thread/thread/571e875c9c4b806f/7ece8e94d48f6586?lnk=gst&q=csrf#7ece8e94d48f6586) and it seems that setting a CSRF cookie can only be done from a site with the same top-level domain.  Hence, this is only an issue if someone malicious controls a subdomain.  In their examples, they are hand-crafting the HTTP request to spoof this but I guess that is not representative of what can be done via a browser-based CSRF attack.  How much of a security issue is this?

I'm happy to share the relevant pages of the report if anyone's interested.

All thoughts appreciated.

-- 
Dr. David Winterbottom
Head of Programming

Tangent Labs
84-86 Great Portland Street
London W1W 7NR
England, UK


Paul McMillan

unread,
Dec 1, 2011, 8:19:07 PM12/1/11
to django-d...@googlegroups.com
Hi David,

Our CSRF protection is a bit different from that implemented by many
other frameworks. The recommendations we (wearing my OWASP hat) make
as OWASP tend to be conservative and lean towards "safe is better than
sorry." Security and pentest companies tend to make similar
recommendations because they don't have the resources to make
absolutely certain that everything is implemented correctly.

In many frameworks, sessions and CSRF are inextricably linked. You
can't have a form without starting a session. Because Django gets used
in so many different ways, we provide an implementation that allows
you to have CSRF protection without sessions (and without storing any
data persistently server-side). This is useful, for example, if you
have a cluster of machines processing requests behind a load balancer-
you don't have to have keep server-side data in sync across the
cluster.

The way our CSRF tokens work is pretty simple. Each form contains a
CSRF token, which matches the CSRF cookie. Before we process the
protected form, we make sure that the submitted token matches the
cookie. This is a server-side check, but it's not validating against a
stored server-side value. Since a remote attacker should not be able
to read or set arbitrary cookies on your domain, this protects you.

Since we're just matching the cookie with the posted token, the data
is not sensitive (in fact it's completely arbitrary - a cookie of
"zzzz" works just fine), and so the rotation/expiration
recommendations don't make any difference. If an attacker can read or
set arbitrary cookies on your domain, all forms of cookie-based CSRF
protection are broken, full stop.

Generating a new token for each request is problematic from a UI
perspective because it invalidates all previous forms. Most users
would be very unhappy to find that opening a new tab on your site
invalidated the form they'd just spent time filling out in the other
tab, or that a form they accessed via the back button could not be
filled out.

That said, there are a few conditions that need to be met in order to
have Django's CSRF protection work to the fullest extent:

1) Use HTTPS. Use it on your entire site. Use it all the time.
Redirect to the encrypted version for all unencrypted requests. If you
don't do this, no CSRF protection in the world can protect you from a
man-in-the-middle.

2) Use HSTS. Set it for several months. Use "includeSubDomains". This
means that no matter what your users type, and no matter what the
man-in-the-middle does, your users will always access your site
securely if they've been there at least once before.

3) Validate the HOST header in your httpd. Don't allow arbitrary
requests to fall through to Django. Serve your site only for the
appropriate domain. See the most recent security advisory for more
information about this.

If you do these 3 things, you'll be able to take advantage of the
other feature that makes the CSRF protection stronger - strict referer
checking (only enforced over HTTPS). This means that even if a
subdomain can set or modify cookies on your domain, they can't force a
visitor to post to your application, since that request won't come
from your own exact domain.

You should make every effort to avoid allowing subdomains to set or
modify arbitrary cookies, but Django's CSRF protection prevents an
attacking subdomain from causing your users to submit authenticated
posts.

Please feel free to come find me on IRC if you want to chat more about
this - I'm PaulM there and I'm usually in #django-dev. You can also
email me directly if you want to talk in private about your specific
deployment.

So, the tl;dr version: Use HTTPS and HSTS. The recommendations you
received are generally good, but aren't relevant to Django's CSRF
protection.

-Paul

--------
As always, if you think you have found a security issue with Django,
please email secu...@djangoproject.com, rather than posting to the
public lists or the bug tracker.
--------

Ryan McIntosh

unread,
Dec 2, 2011, 9:08:29 AM12/2/11
to django-d...@googlegroups.com
Good morning,

I have also looked into this mechanism and I wanted to add to the discussion. Many of my thoughts regard not simply Django's "stock" behavior, but the ease with which a developer may unexpectedly expose their work to security vulnerabilities.

One important note to remember is that an application _can_ inject a CSRF cookie above the domain via settings.CSRF_COOKIE_DOMAIN, settings.CSRF_COOKIE_PATH and settings.CSRF_COOKIE_SECURE.

This places a great onus of responsibility on the developer to ensure that this cookie is never leaked or set above an appropriate domain.

An example where this is dangerous would be a site with multiple subdomains using the same django instance and multiple applications to serve the same site. Because the cookie is available to client side applications as well as server side applications which are potentially outside the developer's control, a potential to exploit or leak the CSRF is present if the aforementioned settings are not secure.

A new token isn't generated for each request because in the case of an application exception, it's important to not leave the user stranded. This could be worked around by storing a set of valid CSRF tokens at the database level and expiring them as needed. Potentially, this would be a good way to expire CSRF tokens prior to the cookie expiration as well - while at the same time ensuring the session hasn't been hijacked. Such a middleware seems to be what Dr. Winterbottom is looking for to resolve his security concerns. The implementation would be rather straightforward as well. Is there value right now in developing something like this for the community at large?

peace,

Ryan McIntosh
Software Architect
PeaceWorks Technology Solutions
ph: (204) 480-0314
cell: (204) 770-3682
ry...@peaceworks.ca

----- Original Message -----
From: "David Winterbottom" <david.win...@tangentlabs.co.uk>
To: django-d...@googlegroups.com
Sent: Thursday, December 1, 2011 3:19:27 PM GMT -06:00 US/Canada Central
Subject: Queries about CSRF design following penetration test

All,

A site I work on was penetration tested this week and several queries were
raised about the site's (and hence Django's) CSRF implementation. The
points seem valid to a degree but I wanted to check if there were design
decisions behind the current implementation.

Note, we're using Django 1.3.1.

*CSRF tokens are not generated per-request or with a max age*


Django's CSRF token is only generated when the cookie is not found, and as
the cookie is set to a max age of a year, the token remains the same
between requests (and visits). Is there a reason why a new token isn't
generated for each request? I appreciate that this doesn't really open up
a huge security hole, but it does differ from OWASP's recommendations:
https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)

The security company also recommend that the CSRF token should have a
maximum age so that it expires if you wait too long.

*No server-side check of the CSRF token*


Basically, as long as the cookie token matches the form token, the request
is valid - even if they differ from what was set originally. I found some
previous discussion of this (
http://groups.google.com/group/django-developers/browse_thread/thread/571e875c9c4b806f/7ece8e94d48f6586?lnk=gst&q=csrf#7ece8e94d48f6586)
and it seems that setting a CSRF cookie can only be done from a site with
the same top-level domain. Hence, this is only an issue if someone
malicious controls a subdomain. In their examples, they are hand-crafting
the HTTP request to spoof this but I guess that is not representative of
what can be done via a browser-based CSRF attack. How much of a security
issue is this?

I'm happy to share the relevant pages of the report if anyone's interested.

All thoughts appreciated.

--
*Dr. David Winterbottom*
Head of Programming

Tangent Labs
84-86 Great Portland Street
London W1W 7NR
England, UK

--
You received this message because you are subscribed to the Google Groups "Django developers" group.
To post to this group, send email to django-d...@googlegroups.com.
To unsubscribe from this group, send email to django-develop...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/django-developers?hl=en.

Luke Plant

unread,
Dec 2, 2011, 11:29:04 AM12/2/11
to django-d...@googlegroups.com
On 01/12/11 21:19, David Winterbottom wrote:
> All,
>
> A site I work on was penetration tested this week and several queries
> were raised about the site's (and hence Django's) CSRF implementation.
> The points seem valid to a degree but I wanted to check if there were
> design decisions behind the current implementation.
>
> Note, we're using Django 1.3.1.
>
> *CSRF tokens are not generated per-request or with a max age*

> Django's CSRF token is only generated when the cookie is not found, and
> as the cookie is set to a max age of a year, the token remains the same
> between requests (and visits). Is there a reason why a new token isn't
> generated for each request? I appreciate that this doesn't really open
> up a huge security hole, but it does differ from OWASP's
> recommendations: https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)

If you generate a new CSRF cookie for each request, then you will run
into problems with forms opened in different tabs/windows. e.g. first
page is opened with form 1, CSRF token 1 in form, and sent CSRF cookie
1. Another page is opened in a different tab with CSRF token 2 and CSRF
cookie 2, which overwrites the cookie globally. Submitting page 2 will
succeed, but page 1 will now fail since it will send CSRF cookie 2 and
CSRF token 1, which don't match.

We have considered a version of our CSRF protection that is integrated
with the session system (as it used to be), which would provide
protection against the cross subdomain attack. However, a mechanism that
allows this to be used optionally instead of the current one is tricky
when it comes to details. (You can swap out the middleware, but the CSRF
decorators are harder, unless you introduce a new setting, which we
don't really want to do).

Also note that if you are giving subdomains to untrusted parties, this
opens you up to cross-subdomain session fixation attacks. Because of
this, supporting the untrusted-subdomain scenario has not been a
priority for us.

Paul addressed the other points I think.

Regards,

Luke

--
The fashion wears out more apparel than the man.
-- William Shakespeare

Luke Plant || http://lukeplant.me.uk/

Sri

unread,
Dec 2, 2011, 2:41:27 PM12/2/11
to django-d...@googlegroups.com
Paul's summary was - HTTPS, HSTS and validate Host header. I will add - you *must* also ensure there are no XSS vulnerabilities on your website.

If your website has a XSS vulnerability, there can be no CSRF protection. This is because XSS makes it possible to steal the csrf as well as session cookie. 

Now Django HTML escapes content by default. But you should be aware that this isn't sufficient to prevent XSS. For example, if you insert dynamic content as part of a html attribute, or as part of a javascript string - django's default protection isn't sufficient.

Refer to the OWASP's XSS prevention cheat sheet - https://www.owasp.org/index.php/XSS_(Cross_Site_Scripting)_Prevention_Cheat_Sheet. The rules to escape content vary depending on where you insert dynamic content. Django's escaping is sufficient for Rule #1 in the cheatsheet. But it is NOT sufficient, and even incorrect, if you insert dynamic data in places such as HTML attributes or Javascript quoted strings or JSON objects. 

This isn't Django's limitation though. Templates cannot figure out the context in which the author is inserting dynamic content. So, in a nutshell, you should be careful 

Paul McMillan

unread,
Dec 2, 2011, 4:44:30 PM12/2/11
to django-d...@googlegroups.com
Sri:

Of course you must make sure there's no XSS. You also must make sure
there's no remote code execution, and that your memcached servers
aren't running unauthenticated on a publicly exposed port.

> If your website has a XSS vulnerability, there can be no CSRF protection.
> This is because XSS makes it possible to steal the csrf as well as session
> cookie.

If there's an XSS vulnerability, it doesn't matter AT ALL that the
CSRF cookie can be stolen, because an XSS can be used to directly
submit malicious forms, using the existing session and CSRF cookies,
even if they're both set to httpOnly. [1] The browser submits them
with every request to your domain.

> this isn't sufficient to prevent XSS. For example, if you insert dynamic
> content as part of a html attribute,

Wrong. What you meant was "as part of an UNQUOTED html attribute".

As the security document very clearly says, DON'T EVER DO THAT. It's
the first thing right at the top there. I'll link to it, in case
anyone missed that.
https://docs.djangoproject.com/en/dev/topics/security/#cross-site-scripting-xss-protection

Always use quotes around your HTML attributes. If you do that,
inserting Django's escaped content into HTML attributes is safe. If
you use unquoted HTML attributes, you should go fix your sites right
now.

>or as part of a javascript string -

The best way to avoid XSS in those situations is to NEVER EVER do
that. Use Django's XSS prevention for HTML, and serialize javascript
values as JSON. You probably want to load them asynchronously, so your
javascript files can be cached (you weren't writing raw javascript
directly into your HTML, were you?)[2].

Serialize the raw Python data structures directly into JSON, rather
than constructing JSON by hand. Python has a good JSON serializer, and
Django includes one if you are using an old version of Python.

As you said, Django's HTML escaping doesn't escape Javascript. That's
what JSON is for. And of course, always use a JSON parser to parse the
JSON (built into most modern browsers and javascript frameworks),
rather than doing eval().

> This isn't Django's limitation though. Templates cannot figure out the
> context in which the author is inserting dynamic content. So, in a nutshell,
> you should be careful

Yep. It's always important to be careful. Sorry for the extensive
reply, but security is such a rabbit hole, it's easy to jump from one
topic to another till you're talking about something completely
different.

Best,
-Paul

[1] Django 1.4 sets the session cookie to httpOnly by default, making
it much harder to steal via XSS.

[2] The ability to write javascript directly into HTML will eventually
go away, when CSP gains broad acceptance. This will alleviate most XSS
problems, but requires a more strict separation of content from
scripting.

Reply all
Reply to author
Forward
0 new messages