Cookieless Django: Sessions and authentication without cookies
Django's session implementation requires cookie support on the browser. It doesn't fall back to putting session IDs in the query string, the way PHP might do. There's a very good reason for this, and it's mentioned at the bottom of the sessions documentation:
The Django sessions framework is entirely, and solely, cookie-based. It does not fall back to putting session IDs in URLs as a last resort, as PHP does. This is an intentional design decision. Not only does that behavior make URLs ugly, it makes your site vulnerable to session-ID theft via the “Referer” header.
Note particularly the last sentence - session IDs in URLs leaves your visitors more vulnerable to Flash-based multiple file upload widget. This is pretty easy to do: after including the required Javascript files, you pop one of these in your page:
var uplooad = new FancyUpload(input, {
swf: '{{ MEDIA_URL }}/Swiff.Uploader.swf',
url: 'http://localhost:8000/1/images/upload/'
/* options */
});
(MEDIA_URL is a Django thing).
Note the upload URL location - this is where the upload widget will POST the files to. This works pretty much as expected.
The problem comes when I protected the image upload view (using the @permission_required decorator from django.contrib.auth.decorators). Permission checks require authentication; authentication requires sessions; sessions require cookies. And the upload widget doesn't post any cookies, even when posting back to the same domain. I'm not sure whether this is a bug or a security feature. Either way, you end up in the authentication machinery with an Anonymous user. Django then helpfully serves up a 302 redirect to your login page.
Darn.
The Solution
My solution was to write a piece of custom Django middleware. Middleware lets you plug into the Django request/response pipeline, and perform processing before a view is resolved and processed.
I figured that if I could write a piece of middleware which pulled the session ID out of the query string and put it in the cookies collection before the session and authentication middlewares ran, then I'd be able to get the session machinery to use a URL-based session ID.
I created a FakeSessionCookie middleware to do exactly that. It's pretty simple.
from django.conf import settings
class FakeSessionCookieMiddleware(object):
def process_request(self, request):
if not request.COOKIES.has_key(settings.SESSION_COOKIE_NAME) \
and request.GET.has_key(settings.SESSION_COOKIE_NAME):
request.COOKIES[settings.SESSION_COOKIE_NAME] = \
request.GET[settings.SESSION_COOKIE_NAME]
I then just had to edit my MIDDLEWARE_CLASSES settings in my project's settings.py to include a reference to this middleware:
MIDDLEWARE_CLASSES = (
'django.middleware.transaction.TransactionMiddleware',
'django.middleware.common.CommonMiddleware',
'mint.middleware.FakeSessionCookieMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.doc.XViewMiddleware',
'django.contrib.flatpages.middleware.FlatpageFallbackMiddleware',
)
Middleware can provide a number of methods. One of these is process_request. It can either return an HttpResponse (in which case no further middlewares are processed) or None, causing the next middleware to be processed as normal. The order of middlewares is therefore important: in this case, the FakeSessionCookie middleware has to be placed before the SessionMiddleware. This lets it inject the fake session cookie into the request.COOKIES collection before the session is processed.
Note that this middleware is conservative: it only injects the query string value into the cookies collection if there isn't already one there.
I then tweaked the script generated for the upload widget, so it looked like this:
var uplooad = new FancyUpload(input, {
swf: '{{ MEDIA_URL }}client-dev/Swiff.Uploader.swf',
url: '{{ BASE_URL }}{% url mint.propertylisting.views.uploadImage
listing.id %}/?{{session_cookie_name}}={{session_cookie_value}}'
});
(BASE_URL is provided by another custom context processor which injects the base absolute URL part of the full request URL; you may need remove the line break before listing.id, that's just for site formatting; note to self, fix CSS!).
The final piece of the puzzle is to put the session_cookie_name and session_cookie_values in the context for the view. This was pretty trivial:
@permission_required('propertylisting.add_propertylistingimage')
def uploadImages(request, property_id):
...
context = {
'session_cookie_name': settings.SESSION_COOKIE_NAME,
'session_cookie_value': request.COOKIES[settings.SESSION_COOKIE_NAME]
}
return render_to_response('propertylisting/uploadImages.html', \
RequestContext(request, dict=context))
That completed the puzzle - I then had an authenticated user in the view handling the image upload.
Is this approach safe?
Well - the short answer is 'no'. URLs - and therefore the session IDs - are being transmitted in the clear with the request.
However, there's little danger of the session ID appearing in some other site's referrer logs. (I won't say no danger, because you never quite know exactly what the client browser will do!). The upload request is coming from the Flash component, not the browser; the browser never 'knows' about the upload URL in any meaningful sense.
I'd therefore consider this method to be relatively safe in this usage. As ever, you have to use your own judgement.