8

I have a Django website at http://example.com that works fine, including post requests. I've added HTTPS so my site is accessible at https://example.com too.

I can load any page on HTTPS, but I always get CSRF validation errors when I try to POST. POST requests work fine on HTTP.

My Django process is running with gunicorn behind nginx, and I have nginx setting X_Forwarded_For. So, an HTTPS request has the following headers (taken from request.META):

'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'HTTP_ACCEPT_ENCODING': 'gzip, deflate',
'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.5',
'HTTP_CONNECTION': 'close',
'HTTP_COOKIE': 'redacted',
'HTTP_HOST': 'example.com:80',
'HTTP_REFERER': 'https://example.com/user/delete/49/',
'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0',
'HTTP_X_FORWARDED_FOR': '1.2.3.4, 192.168.252.22',
'HTTP_X_FORWARDED_PROTO': 'https',
'HTTP_X_REAL_IP': '1.2.3.4',

and an HTTP request has the following headers:

'HTTP_ACCEPT': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'HTTP_ACCEPT_ENCODING': 'gzip, deflate',
'HTTP_ACCEPT_LANGUAGE': 'en-US,en;q=0.5',
'HTTP_CONNECTION': 'close',
'HTTP_COOKIE': 'redacted',
'HTTP_HOST': 'example.com.com:80',
'HTTP_USER_AGENT': 'Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0',
'HTTP_X_FORWARDED_FOR': '1.2.3.4, 192.168.252.22',
'HTTP_X_FORWARDED_PROTO': 'http',
'HTTP_X_REAL_IP': '1.2.3.4',

Why doesn't CSRF work on HTTPS when I have no problems over HTTP?

Wilfred Hughes
  • 29,846
  • 15
  • 139
  • 192

6 Answers6

11

Turns out it was an nginx configuration issue. My server setup is:

nginx -> nginx -> gunicorn

On the second nginx system, I had

proxy_set_header        Host            $host:$server_port;

However, since HTTPS is terminated at the first nginx, $server_port was always 80.

On HTTPS, Django does strict referer checking (see point 4). Looking at the Django source:

good_referer = 'https://%s/' % request.get_host()
if not same_origin(referer, good_referer):
    reason = REASON_BAD_REFERER % (referer, good_referer)
    logger.warning('Forbidden (%s): %s', reason, request.path,
        extra={
            'status_code': 403,
            'request': request,
        }
    )
    return self._reject(request, reason)

CSRF validation was failing because "https://example.com/" != "https://example.com:80/".

Wilfred Hughes
  • 29,846
  • 15
  • 139
  • 192
3

Like mentioned here I solved this problem by adding the following Django constants in settings.py so Django considers proxy headers:

# Setup support for proxy headers
USE_X_FORWARDED_HOST = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
Florian Sager
  • 660
  • 8
  • 14
0

Your CSRF_TRUSTED_ORIGINS setting is wrong - change it to:

CSRF_TRUSTED_ORIGINS = ['similarchords.com', 'www.similarchords.com'] 
Sanjay Sikdar
  • 435
  • 4
  • 10
0

In my case I had a public host with HTTPS but the internal host was HTTP so forwarding https://myhost.com/path/ to http://internal-host.myhost.com/path failed. Here is how the config was:

location @proxy_to_app {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    # the following means the $scheme e.g. https is forwarded
    # disabling this fixes the problem
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Host $http_host;
    proxy_redirect off;
    proxy_pass http://app_server;
}
polarise
  • 2,303
  • 1
  • 19
  • 28
0

this code worked for me

CSRF_TRUSTED_ORIGINS = [
    'https://domain name',
    
]

SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SESSION_COOKIE_SECURE = True
Virb
  • 1,639
  • 1
  • 16
  • 25
-2

If you look at the implementation of CsrfViewMiddleware

Django checks for 'Referer' header when request.is_secure()

good_referer = 'https://%s/' % request.get_host()
if not same_origin(referer, good_referer):
    reason = REASON_BAD_REFERER % (referer, good_referer)
    return self._reject(request, reason)
Nam Ngo
  • 2,093
  • 1
  • 23
  • 30