Opened 8 years ago

Closed 8 years ago

Last modified 8 years ago

#27958 closed Bug (invalid)

CSRF_COOKIE reset while requesting a broken relative URL over HTTPS

Reported by: cryptogun Owned by: nobody
Component: CSRF Version: 1.10
Severity: Normal Keywords: csrf reset https 403
Cc: Triage Stage: Unreviewed
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

Problem: If a comment contains a broken link (under the same domain), all csrf_token are expired and any POST method would get a 403 code.
This also happen if I first open https://localhost/ and then open https://localhost/non-exist/ in another tab, now I can't make POST on the first page.
Everything was OK, while I was using HTTP.

GET https://192.168.1.2/asdf.jpg
Cookie: csrftoken=BBBB
...
status: 404
set-cookie: csrftoken=CCCC

-------------------------------------------------------
POST https://192.168.1.2/forum/comment/bookmark/163/create/
comment_number: 1
csrfmiddlewaretoken: DDDD

Cookie: csrftoken=CCCC
...
status: 403

I'm using https + nginx + gunicorn.

After some debugging, I found that "CSRF_COOKIE" is not in request.META inside context_processors.py:

render(request, html404)
context_processors.py get_token(request)
    if "CSRF_COOKIE" not in request.META:
        csrf_secret = _get_new_csrf_string()
        request.META["CSRF_COOKIE"] = _salt_cipher_secret(csrf_secret)
        response.set_cookie(settings.CSRF_COOKIE_NAME,
                            request.META["CSRF_COOKIE"],

So the shared reset new csrf_cookie doesn't match with the old static html csrf_token, resulting a 403 Forbidden page.
More detail [here](https://github.com/nitely/Spirit/pull/173).
Test: https://code.djangoproject.com/non-exist

Change History (6)

comment:1 by Tim Graham, 8 years ago

I'm not sure there's a bug in Django here. Why isn't the CSRF token sent with the request for the broken relative link?

in reply to:  1 comment:2 by cryptogun, 8 years ago

Replying to Tim Graham:

I'm not sure there's a bug in Django here. Why isn't the CSRF token sent with the request for the broken relative link?

New CSRF token did sent and stored in cookie (for the broken relative link).
But manage.py check --deploy suggest me to set this to true: CSRF_COOKIE_HTTPONLY = True

If True, client-side JavaScript is not able to access the CSRF cookie. This can help prevent malicious JavaScript from bypassing CSRF protection.

But {{ csrf_token }} are static in pages(and ajax javascripts) that're already opened previously.

            if not _compare_salted_tokens(request_csrf_token, csrf_token):
                return self._reject(request, REASON_BAD_TOKEN)

But why I can POST and submit comment here? djangoproject also use HTTPS.

Last edited 8 years ago by cryptogun (previous) (diff)

comment:3 by Tim Graham, 8 years ago

Resolution: invalid
Status: newclosed

If disabling CSRF_COOKIE_HTTPONLY fixes the issue, that should be fine -- it offers little practical security, see #27611. If you want to keep it enabled, then you need to submit AJAX requests as described in #27534.

Next time, you might want to ask on our support channels unless you are really sure the issue is a bug. As far as I can tell, there's no issue here. Thanks.

in reply to:  3 comment:4 by cryptogun, 8 years ago

Replying to Tim Graham:

I thought this ticket is closed and not commentable until I login now and see the input area.

If disabling CSRF_COOKIE_HTTPONLY fixes the issue

No that would not fix the issue. It's just a workaround. CSRF_COOKIE_HTTPONLY = False I still can't make the POST.
In order to make POST I should:

  • 1. Set CSRF_COOKIE_HTTPONLY = False
  • 2. Remove all {{ csrf_token }}
  • 3. Add at the end of the html body:
<script>
function getCookie(cname) {
    var name = cname + "=";
    var decodedCookie = decodeURIComponent(document.cookie);
    var ca = decodedCookie.split(';');
    for(var i = 0; i <ca.length; i++) {
        var c = ca[i];
        while (c.charAt(0) == ' ') {
            c = c.substring(1);
        }
        if (c.indexOf(name) == 0) {
            return c.substring(name.length, c.length);
        }
    }
    return "";
}

$('form').submit(function(event) {event.preventDefault(); newform = $(this).serialize();alert(newform + "&csrfmiddlewaretoken=" + getCookie('csrftoken')); this.submit(); return false;})
</script>

If you want to keep it enabled, then you need to submit AJAX

No. I have to disable it in order to submit AJAX successfully.

comment:5 by Tim Graham, 8 years ago

Sorry, but I'm still not following what you're saying. If you can submit a patch with a test, perhaps that will explain the issue more clearly.

comment:6 by cryptogun, 8 years ago

My fault. I forgot to add a requires_csrf_token decorator to my custom 404 handler. Sorry for the bothering.
Quote from Django code:

# This can be called when CsrfViewMiddleware.process_view has not run,
# therefore need @requires_csrf_token in case the template needs
# {% csrf_token %}.
@requires_csrf_token
def page_not_found(

My custom 404 handler used to be:

def handler404(request):
    html404 = {'general': 'homepage/404.html',
        'app1': 'homepage/app1_not_found.html'}

    if request.path.startswith('/app1/'):
        response = render(request, html404['app1'],
            # context=RequestContext(request)
        )
    else:
        response = render(request, html404['general'],
            # context=RequestContext(request)
        )
    response.status_code = 404

    return response

Solution 1, use built-in page_not_found:

def handler404(request, exception):
    if request.path.startswith('/app1/'):
        template_name='homepage/app1_not_found.html'
    else:
        template_name='homepage/404.html'
    return page_not_found(request, exception, template_name=template_name)

Solution 2, uncommit RequestContext(request) to get csrftoken from request. So Django won't generate a new csrftoken.
Either way solves the problem.

I learned that code from here and here, but omitted the crucial parts.

Note: See TracTickets for help on using tickets.
Back to Top