Search code examples
jquerydjangouploadify

Can i get Uploadify to work with Django >=1.2.5 using the script-data option to pass CSRF-validation?


Due to the change in CSRF-policy for AJAX requests since Django 1.2.5, all implementations of Uploadify seem to be broken. There are a few attempts and some pretty blurry answers, but no solution yet. The only workaround right now seems to be using the @csrf_exempt decorator, as pointed out in this post: Fixing django csrf error when using uploadify

Allthough Paul McMillan has pointed the reason for this issue, he did not propose a solution to this (except learning Actionscript and rewriting Uploadify). For anyone using Django and jQuery it would be interesting to get a bit more specific on this topic, as not everyone has the time to learn actionscript. I am especially curious if there could be a solution using the script-data option of Uploadify, which i could not get to work.

$('#fileInput').uploadify({
'uploader'  : '{{ uploadify_path }}uploadify.swf',
'script'    : '{% url uploadify_upload %}',

//this is the interesting line
'scriptData': {"CSRF-Token" : $('input[name="csrfmiddlewaretoken"]').val()}
                },
'cancelImg' : '{{ uploadify_path }}cancel.png',
'auto'      : false,
'folder'    : '{{ upload_path }}',
'multi'     : true,
'onAllComplete' : allComplete
});

I thought this could work, the data specified in the script-data option does appear in the request.POST dict. I check for that with pdb and looking for request:

@csrf_exempt
def upload(request, *args, **kwargs):
    if request.method == 'POST':
        if request.FILES:
            upload_received.send(sender='uploadify', data=request.FILES['Filedata'])
    import pdb; pdb.set_trace();
    return HttpResponse(request)  

And this is the result:

<WSGIRequest
GET:<QueryDict: {}>,
POST:<QueryDict: {u'CSRF-Token': [u'de885c962f9a2e50fec140e161ca993e'], u'folder': [u'/static/uploads/'], u'Upload': [u'Submit Query'], u'Filename': [u'P4010040.JPG']}>,
COOKIES:{},
META:{'App 

and so on, the rest as expected  

This is almost the same solution as proposed in an answer to the previously mentioned post, but that solution would break CSRF-protection. Can i somehow use scriptData to pass the CSRF-validation, without breaking the protection? Which information would i need to pass the validation, and how can i use it?

edit:
The post i mentioned uses this solution, that breaks the csrf-protection:

Javascript:

biscuit = document.cookie;
csrt = $('input[name="csrfmiddlewaretoken"]').val();
$('#file_upload').uploadify({
      // pass the cookie and the csrftoken
      scriptData : {biscuit: biscuit, csrfmiddlewaretoken: csrf},
      .... // other codes
 });

Middleware:

#insert after: 'django.middleware.common.CommonMiddleware'
def process_request(self, request):
    if (request.method == 'POST'):
       if request.POST.has_key('biscuit'):
          biscuit = request.POST['biscuit']
          tmp = map(lambda x: tuple(x.split("=")), biscuit.split(" "))
          # set a cookie
          request.COOKIES.update(tmp)

What if there would be a direct check for the correct value of csrfmiddlewaretoken and session_id? The main issue is that Djangos CSRF protection relies on a CSRF cookie, and uploadify doesn't pass the cookie. But it can pass the values of the csrfmiddlewaretoken and the session_id via scriptData. Wouldn't it preserve the CSRF protection telling Django not to look for the csrf-cookie, but for the relevant values inside request.POST?

What i essentially wanted to say: Do not set "biscuit" blindly, but after a check for the important values (csrfmiddlewaretoken, sessionid, what else?). I think that could work, allthough i am not sure i have fully understood the mechanism of csrf protection...


Solution

  • I had the same issue as you have.

    As of Django 1.2.5, Django checks CSRF on ALL requests to it. The reason is because Google folks found a way how to forge a request to any URL with a custom header. So now the only way to validate the Django CSRF is by either having a CSRF_token cookie, or by sending a X-CSRFToken header which will has the value of the CSRF token. The release notes regarding this can be found here.

    From what I understand so far it is impossible to fix it in Uploadify because Uploadify uses SWFObject to actually send data, which is Flash and Flash does not allow to add custom headers.

    This uploader however works purely by using XHR object to send data or falls back to iFrame for non-supporting browsers (I did not had a change to check the solution though when it falls back to iFrame however it works perfectly when using XHR object). In addition it is a jQuery based. Here is an demo implementation of how this uploader is implemented in Django however it is still CSRF exempt.

    The way to enable CSRF validation for this demo is by adding a JS snipped for jQuery from the Django docs (here). What that snipped does it that it overwrites the default AJAX behavior in jQuery and on each AJAX request adds the custom header (X-CSRFToken) with the value of the CSRF token. Now since the uploader is jQuery based, all the AJAX requests it makes will be CSRF valid.

    Here is the snipped (again from Django docs):

    $(document).ajaxSend(function(event, xhr, settings) {
        function getCookie(name) {
            var cookieValue = null;
            if (document.cookie && document.cookie != '') {
                var cookies = document.cookie.split(';');
                for (var i = 0; i < cookies.length; i++) {
                    var cookie = jQuery.trim(cookies[i]);
                    // Does this cookie string begin with the name we want?
                    if (cookie.substring(0, name.length + 1) == (name + '=')) {
                        cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                        break;
                    }
                }
            }
            return cookieValue;
        }
        function sameOrigin(url) {
            // url could be relative or scheme relative or absolute
            var host = document.location.host; // host + port
            var protocol = document.location.protocol;
            var sr_origin = '//' + host;
            var origin = protocol + sr_origin;
            // Allow absolute or scheme relative URLs to same origin
            return (url == origin || url.slice(0, origin.length + 1) == origin + '/') ||
                (url == sr_origin || url.slice(0, sr_origin.length + 1) == sr_origin + '/') ||
                // or any other URL that isn't scheme relative or absolute i.e relative.
                !(/^(\/\/|http:|https:).*/.test(url));
        }
        function safeMethod(method) {
            return (/^(GET|HEAD|OPTIONS|TRACE)$/.test(method));
        }
    
        if (!safeMethod(settings.type) && sameOrigin(settings.url)) {
            xhr.setRequestHeader("X-CSRFToken", getCookie('csrftoken'));
        }
    });
    

    Hope this helps.