Posts in "Django"
More Django Paranoia
As Ryan pointed out in response to my previous post on django-paranoid-sessions, the only way to truly prevent sniffing or man-in-the-middle attacks is to operate over a secure connection. Fair enough, but HTTPS ain't free. The general consensus seems to be that a secure connection is too much overhead for anything but the high-value or high-risk sections of your website (login submissions, payment processing, nuclear launch codes, etc).
Ideally, it should be possible to place selected sections of your website behind a secure connection and gain added attack-resistance for those sections, while still sharing session data with the rest of the site. Using a recommendation from the OWASP session management guide, the latest release of django-paranoid-sessions now lets you do exactly that.
The idea is to maintain a second randomly-generated session key that is only sent when the client connects over a secure channel. Unencrypted requests within your session are oblivious to the second key, but if a secure request doesn't provide both valid session keys then it is rejected. You can think of this extra key as a second "security enhanced" session that transparently piggybacks on top of the standard session data.
Like most web frameworks, Django provides a convenient mechanism for storing data across requests in a persistent "session" object. Like most web frameworks, Django implements sessions using a simple mapping from a "session key" to a session object stored on the server. And like most web frameworks, Django's default session implementation is trivially vulnerable to session hijacking attacks.
Django's session implementation is quite similar to that provided by PHP; for all the gory details here is an excellent article on The Truth about Sessions, but the simplified version is as follows. When you first visit a Django-powered site, the server generates a random "session key" and returns it to your browser in a cookie. Any data that the server wants to remember about you (say, whether you have logged in and under what username) is stored in a giant dictionary indexed by the session key. On each subsequent visit you browser sends the key back to the server, which looks up your data in this dictionary and proceeds merrily on its way. The interaction looks something like the following:
- You login at the (hypothetical) Django-powered website http://www.my-todo-list.com/.
- The server stores your login details in its session database, and sends back a session key of "123456".
- You send a request to update your todo list, presenting a session key of "123456".
- The server looks up "123456" in its session database, checks that the session is correctly logged in as you, and proceeds with the requested update.
It's a simple and convenient mechanism, but it has an important security issue: anyone who knows your session key can impersonate you to the server! Consider what happens next:
Deploying Django projects is in general a straightforward affair, but it still suffers from a pain-point that's as old as web apps themselves: deploying at an arbitrary root URL. In my ideal world, I would push my shiny new Django project to the server, instruct Apache to mount it at "/my/shiny/app", and everything would just work – all URLs would magically have "/my/shiny/app" stripped off on their way into Django and prepended again on their way out. In the real world, Django comes pretty close to this ideal but stops just far enough short to be annoying.
First, here's what Django gets right: reverse(), permalink() and {% url %} are awesome. They introspect Django's runtime environment to translate an application-level name or object into a deployment-level URL. Your applications have no excuse for hard-coding URLs or even URL fragments. In theory, these two functions should be enough to make Django completely agnostic about its deployment location.
Now here's what Django gets wrong: some of its core components don't use them. Instead they use hard-coded URLs defined in the settings module, such as settings.ADMIN_MEDIA_PREFIX and settings.LOGIN_URL. Attempts to patch these components to avoid hard-coded URLs have been closed wontfix, so I guess we're stuck with them for a while.
Following my previous post on testing Django with Windmill, I quickly ran into a common snag with in-browser web app testing: it's not possible to programmatically set the value of file input fields. This makes it very difficult to test file upload functionality using frameworks such as Windmill or Selenium.
In Firefox it's possible to request elevated permissions for your unit tests, but this is far from ideal. It means the tests are no longer automatic (you have to click "yes, grant this page extra permissions" whenever the tests are run) and it takes other browsers out of the testing loop. Like many things in life, the easiest solution seems to be simply to fake it.
But like any convincing fakery, the details are never that simple in practice. Uploading a big file from a web browser will take a long time, but could be nearly instantaneous if you fake it using a server-side file. And what if you have custom upload handlers to enable things like upload progress reporting? How can we make fake file uploads as transparent and convincing as possible?
I've been having my mind blown by Django over the course of this week. Not the in flashy one-shining-moment-of-brilliance kind of way, but through a slowly dawning awareness of just how much it makes possible. Or perhaps it's more accurate to say: just how much I need to re-calibrate my expectations of what should be possible, and what should be downright easy. My latest little epiphany has revolved around unit-testing, which back when I was cutting my teeth on PHP4 was far from a trivial undertaking for even a simple web-app.
While developing my various Python libraries, I've become accustomed to the straightforward testing support in setuptools. This allows a simple python setup.py test to run the full suite of unittests against a clean build of the code, including an automatic re-build of any C extensions if required. But testing libraries is one thing, and testing an interactive application is quite another. Suffice to say, I was expecting to be in for a good deal of pain.
A quick Google for "Django unittest" later, and I was feeling a lot more optimistic. The top result is the official Django docs page on testing Django applications, and it's a pretty decent overview of the bundled testing framework. They've done a great job of leveraging the standard unittest module to make testing a Django application feel much like testing any other Python project – you write tests inside subclasses of unittest.TestCase, and a simple python manage.py test runs them in a clean build of the code, including setup of a separate test database. All in all, remarkably pain free.