adamchainz / django-cors-headers

Django app for handling the server headers required for Cross-Origin Resource Sharing (CORS)
MIT License
5.36k stars 537 forks source link

No "Access-Control-Allow-Origin" in response despite all being set properly #862

Open SylvainBigonneau opened 1 year ago

SylvainBigonneau commented 1 year ago

Understanding CORS

Python Version

3.10

Django Version

3.2.9

Package Version

3.14.0

Description

Sorry for the very typical issue name, but I am in a real pickle here.

Here is my config:

environment variable: DJANGO_ALLOWED_HOSTS=myapp.com,localhost:3000

settings.py:

INSTALLED_APPS = [
    # ...
    "corsheaders",
    # ...
]

MIDDLEWARE = [
    "corsheaders.middleware.CorsMiddleware",
    # ...
]

allowed_hosts = env("DJANGO_ALLOWED_HOSTS", default=None)
ALLOWED_HOSTS = allowed_hosts.split(",") if allowed_hosts else []

CSRF_TRUSTED_ORIGINS = ALLOWED_HOSTS
CORS_ALLOWED_ORIGINS = ["https://" + h for h in ALLOWED_HOSTS]

For debugging, I also set up some hacky logging at request, using signals. handlers.py

from corsheaders.signals import check_request_enabled
from django.conf import settings

def cors_allow_api_to_everyone(sender, request, **kwargs):
    print("CHECKING REQUEST ORIGIN")
    print(request.headers['Origin'])
    print(','.join(settings.CORS_ALLOWED_ORIGINS))
    return False

check_request_enabled.connect(cors_allow_api_to_everyone)

When requesting a url using curl this way:

curl -H "Origin: example.com" -v "https://myapp.com/api/myprivateroute/"

Here is the output in the server logs:

CHECKING REQUEST ORIGIN
example.com
https://myapp.com,https://localhost:3000

Here is the curl output for the response:

< HTTP/2 403 
< content-type: application/json
< content-length: 58
< date: Tue, 16 May 2023 13:12:04 GMT
< set-cookie: AWSALB=xxxxxxx; Expires=Tue, 23 May 2023 13:12:04 GMT; Path=/
< set-cookie: AWSALBCORS=xxxxxxx; Expires=Tue, 23 May 2023 13:12:04 GMT; Path=/; SameSite=None; Secure
< server: nginx/1.21.0
< vary: Accept, Cookie, Origin
< allow: GET, PUT, DELETE, HEAD, OPTIONS
< x-frame-options: DENY
< x-content-type-options: nosniff
< referrer-policy: same-origin
< x-cache: Error from cloudfront
< via: 1.1 xxxxxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: xxxxxx
< x-amz-cf-id: xxxxxxx

As you can see, no trace of any "Access-Control-Allow-Origin" response header there, despite the server receiving "example.com" as an origin, and the CORS_ALLOWED_ORIGINS settings being properly set.

For further info, I tried forcing returning an "Access-Control-Allow-Origin" using the finalize_response override on one of my DRF methods, and it does return the header properly on the previously mentioned curl command. So it is likely not an issue of the header being lost on the way back to the client.

adamchainz commented 1 year ago

The response has a 403 status code. Where is that coming from? Is it from a middleware above the OCRS middleware?

Also, you have a CORS-ish cookie:

< set-cookie: AWSALBCORS=xxxxxxx; Expires=Tue, 23 May 2023 13:12:04 GMT; Path=/; SameSite=None; Secure

Is your AWS ALB (application load balancer) controlling CORS for you? Perhaps it is stripping request CORS headers.

neverabsolute commented 1 year ago

Encountering the same issue, I have everything set up in my settings.py and it works fine on a single EC2 but when put behind an ALB the headers just seem to vaporize.

Currently just trying to replicate the headers that this library would send in my top level Nginx config. Painful but I think I'm making progress.

SylvainBigonneau commented 1 year ago

Thank you for the quick response @adamchainz!

The response has a 403 status code. Where is that coming from? Is it from a middleware above the OCRS middleware?

Haha, yes, I figured this would be confusing, but my example just happened to be on a confidential route where I was unauthenticated. It should still return the proper cors headers though, right? Anyway, here are similar outputs from a route that returns 200:

< HTTP/2 200 
< content-type: application/json
< content-length: 1745
< vary: Accept-Encoding
< date: Sat, 20 May 2023 14:18:04 GMT
< set-cookie: AWSALB=xxxx; Expires=Sat, 27 May 2023 14:18:04 GMT; Path=/
< set-cookie: AWSALBCORS=xxxx; Expires=Sat, 27 May 2023 14:18:04 GMT; Path=/; SameSite=None; Secure
< server: nginx/1.21.0
< vary: Accept, Cookie, Origin
< allow: GET, HEAD, OPTIONS
< x-frame-options: DENY
< x-content-type-options: nosniff
< referrer-policy: same-origin
< x-cache: Miss from cloudfront
< via: 1.1 xxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: xxxx
< x-amz-cf-id: xxxx

Also, you have a CORS-ish cookie:

< set-cookie: AWSALBCORS=xxxxxxx; Expires=Tue, 23 May 2023 13:12:04 GMT; Path=/; SameSite=None; Secure

Is your AWS ALB (application load balancer) controlling CORS for you? Perhaps it is stripping request CORS headers.

As I mentionned at the end of my issue, I have no issues receiving the proper headers when I set them manually using finalize_response:

def finalize_response(self, request, *args, **kwargs):
    response = super(PublicObjectDetail, self).finalize_response(
        request, *args, **kwargs
    )
    response["Access-Control-Allow-Origin"] = "*"
    response[
        "Access-Control-Allow-Headers"
    ] = "Origin, X-Requested-With, Content-Type, Accept"
    return response

Result from curl:

< HTTP/2 200 
< content-type: application/json
< content-length: 1745
< vary: Accept-Encoding
< date: Sat, 20 May 2023 14:05:12 GMT
< set-cookie: AWSALB=xxxx; Expires=Sat, 27 May 2023 14:05:12 GMT; Path=/
< set-cookie: AWSALBCORS=xxxx; Expires=Sat, 27 May 2023 14:05:12 GMT; Path=/; SameSite=None; Secure
< server: nginx/1.21.0
< vary: Accept, Cookie
< allow: GET, HEAD, OPTIONS
< access-control-allow-origin: *
< access-control-allow-headers: Origin, X-Requested-With, Content-Type, Accept
< x-frame-options: DENY
< x-content-type-options: nosniff
< referrer-policy: same-origin
< x-cache: Miss from cloudfront
< via: 1.1 xxxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: xxxx
< x-amz-cf-id: xxxx

As you can see, I receive the headers in the response. (by the way @neverabsolute , you should try this workaround to check if your issue is indeed due to outside factors such as the load balancer, in which case yours is probably not the same issue as mine)

adamchainz commented 1 year ago

I was asking if the ALB is stripping CORS request headers. Your workaround sets respnse CORS headers unconditionally, but that’s not a generally secure way to do CORS, hence this package only sets them on CORS requests. You could test by debugging the headers received by Django, or read up the AWS docs - I’ve found them mostly complete, if verbose.

neverabsolute commented 1 year ago

My issue somehow ended up being related to me changing my authentication session engine to redis, spent 60+ hours debugging other stuff vs checking that 💀

SylvainBigonneau commented 1 year ago

I was asking if the ALB is stripping CORS request headers. Your workaround sets respnse CORS headers unconditionally, but that’s not a generally secure way to do CORS, hence this package only sets them on CORS requests. You could test by debugging the headers received by Django, or read up the AWS docs - I’ve found them mostly complete, if verbose.

Ah, my bad, I wrongly assumed you meant response headers instead, sorry!

I did read up a good sum of the AWS docs on this issue, and all they seem to talk about as far as I can see are the Origin, Access-Control-Request-Method, Access-Control-Request-Headers headers. As mentioned in the first post, I already checked that Origin was properly forwarded, but here is the same debugging including the other two:

from corsheaders.signals import check_request_enabled
from django.conf import settings

def cors_allow_api_to_everyone(sender, request, **kwargs):
    print("CHECKING REQUEST ORIGIN")
    print(request.headers['Origin'])
    print(request.headers['Access-Control-Request-Method'])
    print(request.headers['Access-Control-Request-Headers'])
    return False

check_request_enabled.connect(cors_allow_api_to_everyone)

When requesting a url using curl this way:

curl -H "Origin: example.com" -H "Access-Control-Request-Method: GET" -H "Access-Control-Request-Headers: X-Requested-With" -v "https://myapp.com/api/myprivateroute/" -X OPTIONS

Here is the output in the server logs:

CHECKING REQUEST ORIGIN
example.com
GET
X-Requested-With

So I am thinking this proves that the headers are being forwarded correctly?

adamchainz commented 1 year ago

🤷‍♂️ I'm sorry, I'm not really sure then.

martinskou commented 10 months ago

Check for a missing CSRF token!

Saad-R-Ahmad commented 8 months ago

@SylvainBigonneau Did you find the solution to this problem? I have got the same issue

Django==4.2.5 django-cors-headers==4.3.0 djangorestframework==3.14.0

All setting done as per the docs:

INSTALLED_APPS = [.., 'corsheaders', ..]
MIDDLEWARE = [..., 'corsheaders.middleware.CorsMiddleware',...]
CORS_ALLOW_ALL_ORIGINS = True 
CORS_ALLOW_HEADERS = default_headers

no sign of 'Access-Control-Allow-Origin' header when I do a curl: image

P.S: The Django application is behind an nginx server with proxy pass set:

location /api{ proxy_pass http://backend:8000; }

msdqhabib commented 8 months ago

@SylvainBigonneau Did you find the solution to this problem? I have got the same issue

Django==4.2.5 django-cors-headers==4.3.0 djangorestframework==3.14.0

All setting done as per the docs:

INSTALLED_APPS = [.., 'corsheaders', ..]
MIDDLEWARE = [..., 'corsheaders.middleware.CorsMiddleware',...]
CORS_ALLOW_ALL_ORIGINS = True 
CORS_ALLOW_HEADERS = default_headers

no sign of 'Access-Control-Allow-Origin' header when I do a curl: image

P.S: The Django application is behind an nginx server with proxy pass set:

location /api{ proxy_pass http://backend:8000; } Facing same problem. Please let us know if you find a solution for this.

trentsgustavo commented 5 months ago

Any update on this? i have a similar issue

arg3t commented 3 months ago

For anyone who finds this, the screenshot below shows that the Origin header is missing from the request. As specified in the issue 901, you need to add that header to get the access-control-allow-origin header back,

@SylvainBigonneau Did you find the solution to this problem? I have got the same issue

Django==4.2.5 django-cors-headers==4.3.0 djangorestframework==3.14.0

All setting done as per the docs:

INSTALLED_APPS = [.., 'corsheaders', ..]
MIDDLEWARE = [..., 'corsheaders.middleware.CorsMiddleware',...]
CORS_ALLOW_ALL_ORIGINS = True 
CORS_ALLOW_HEADERS = default_headers

no sign of 'Access-Control-Allow-Origin' header when I do a curl: image

P.S: The Django application is behind an nginx server with proxy pass set:

location /api{ proxy_pass http://backend:8000; }

preciousimo commented 2 months ago

I think this issue has been solved!

SylvainBigonneau commented 2 months ago

Sorry for the ghosting people, I must admit I had given up hope on this, and left it up to my finalize_response tweaks. I will give this another go today.

SylvainBigonneau commented 2 months ago

Nope, I can confirm this issue still occurs exactly the same on django-cors-headers v4.4.0:

> GET /api/objects/576/ HTTP/2
> Host: myapp.com
> User-Agent: curl/8.6.0
> Accept: */*
> Origin: example.com
> Access-Control-Request-Method: GET
> Access-Control-Request-Headers: X-Requested-With
> 
< HTTP/2 403 
< content-type: application/json
< content-length: 58
< date: Sat, 06 Jul 2024 14:40:10 GMT
< set-cookie: AWSALB=xxxx; Expires=Sat, 13 Jul 2024 14:40:09 GMT; Path=/
< set-cookie: AWSALBCORS=xxxx; Expires=Sat, 13 Jul 2024 14:40:09 GMT; Path=/; SameSite=None; Secure
< server: nginx/1.21.0
< vary: Accept, Accept-Language, Cookie, origin
< allow: GET, PUT, DELETE, HEAD, OPTIONS
< content-language: fr
< x-frame-options: DENY
< x-content-type-options: nosniff
< referrer-policy: same-origin
< x-cache: Error from cloudfront
< via: xxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: xxxx
< x-amz-cf-id: xxxx
< 
* Connection #0 to host myapp.com left intact
{"detail":"Authentication error"}

Yes, I know it's a 403, because I didn't bother authenticating with curl, and I put all my public routes behind a Cloudfront cache because they are bombarded by requests from clients, so requesting those routes won't actually hit my server. But as far as I can tell, no matter the HTTP code, I should get that sweet sweet access-control-allow-origin response header... and I don't :(

As usual, here is the debug output from the server logs, showing that the request does hit the server with all the proper request headers:

CHECKING REQUEST ORIGIN
example.com
GET
X-Requested-With
https://myapp.com,https://localhost:3000

And if I force the response headers by overriding the finalize_response method of my route:

    def finalize_response(self, request, *args, **kwargs):
        response = super(ObjectDetail, self).finalize_response(
            request, *args, **kwargs
        )
        response["Access-Control-Allow-Origin"] = "https://myapp.com,https://localhost:3000"
        response["Access-Control-Allow-Headers"] = (
            "Origin, X-Requested-With, Content-Type, Accept"
        )
        return response

I get the response header no problem, showing that it is not AWS stripping them off:

> GET /api/objects/576/ HTTP/2
> Host: myapp.com
> User-Agent: curl/8.6.0
> Accept: */*
> Origin: example.com
> Access-Control-Request-Method: GET
> Access-Control-Request-Headers: X-Requested-With
> 
< HTTP/2 403 
< content-type: application/json
< content-length: 58
< date: Sat, 06 Jul 2024 15:02:33 GMT
< set-cookie: AWSALB=xxxx; Expires=Sat, 13 Jul 2024 15:02:32 GMT; Path=/
< set-cookie: AWSALBCORS=xxxx; Expires=Sat, 13 Jul 2024 15:02:32 GMT; Path=/; SameSite=None; Secure
< server: nginx/1.21.0
< vary: Accept, Accept-Language, Cookie, origin
< allow: GET, PUT, DELETE, HEAD, OPTIONS
< access-control-allow-origin: https://myapp.com,https://localhost:3000
< access-control-allow-headers: Origin, X-Requested-With, Content-Type, Accept
< content-language: fr
< x-frame-options: DENY
< x-content-type-options: nosniff
< referrer-policy: same-origin
< x-cache: Error from cloudfront
< via: 1.1 xxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: xxxx
< x-amz-cf-id: xxxx
< 
* Connection #0 to host myapp.com left intact
{"detail":"Authentication error"}

So nope, not solved!

By the way my app behaves, it really feels like django-cors-headers is simply not "running" at all on my server, despite having installed the module (otherwise my debug signal would fail, since I'm importing the module to enable it), set the middleware before django's CommonMiddleware, and setting CORS_ALLOWED_ORIGINS properly (as it shows in my debugging).

I would love to help debugging this further, but I am not well versed in the intrications of debugging a third-party django middleware... If anyone can guide me a little on how and where I could look in my django app to investigate where the middleware is failing, I'd be happy to try it!

Could you clarify what you meant about the "missing CSRF token" @martinskou ? How would I check that from the request object?

arg3t commented 2 months ago

@SylvainBigonneau what happens if you send an OPTIONS http request? Like below:

curl -v -X OPTIONS ...

SylvainBigonneau commented 2 months ago

@SylvainBigonneau what happens if you send an OPTIONS http request? Like below:

curl -v -X OPTIONS ...

> OPTIONS /api/objects/576/ HTTP/2
> Host: myapp.com
> User-Agent: curl/8.6.0
> Accept: */*
> Origin: example.com
> Access-Control-Request-Method: GET
> Access-Control-Request-Headers: X-Requested-With
> 
< HTTP/2 200 
< content-type: text/html; charset=utf-8
< content-length: 0
< date: Sat, 06 Jul 2024 17:50:22 GMT
< set-cookie: AWSALB=xxxx; Expires=Sat, 13 Jul 2024 17:50:22 GMT; Path=/
< set-cookie: AWSALBCORS=xxxx; Expires=Sat, 13 Jul 2024 17:50:22 GMT; Path=/; SameSite=None; Secure
< server: nginx/1.21.0
< vary: origin
< x-cache: Miss from cloudfront
< via: 1.1 xxxx.cloudfront.net (CloudFront)
< x-amz-cf-pop: xxxx
< x-amz-cf-id: xxxx
< 
* Connection #0 to host myapp.com left intact
jesselathamdev commented 2 months ago

I'd like to report that I'm having the same issues as seen here, spent yesterday working on first deploy to production, and ended up forcing the required headers into the response in some middleware. It's like django-cors-headers is just not being included...

API server

Pipfile (prod on DigitalOcean App Platform)

[requires]
python_version = "3.11"

[packages]
django = "==5.0.6"
psycopg = "==3.2.1"
python-dotenv = "==1.0.1"
tzdata = "==2024.1"
djangorestframework = "==3.15.1"
dj-rest-auth = "==6.0.0"
django-cors-headers = "==4.4.0"
django-allauth = "==0.61.1"
# Campaign Monitor API wrapper
createsend = "==7.0.0"
# WSGI HTTP server for Python (and Django) for Production usage
gunicorn = "==22.0.0"
# Static file handling for Django Admin
whitenoise = "==6.7.0"
sentry-sdk = {extras = ["django"], version = "*"}
INSTALLED_APPS = [
    'django.contrib.sites',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'rest_framework.authtoken',
    'allauth',
    'allauth.account',
    'dj_rest_auth',
    'dj_rest_auth.registration',
    'corsheaders',
    'app'
]

middleware

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'whitenoise.middleware.WhiteNoiseMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
    'allauth.account.middleware.AccountMiddleware',
    'app.middleware.InspectRequestMiddleware',
]

I had attempted to move corsheaders.middleware.CorsMiddleware to different positions within the MIDDLEWARE stack but to no avail.

And same response as @SylvainBigonneau (no CORS headers being included).

jesselathamdev commented 2 months ago

Question about enablement of the library for a given response...

How this is block used within the CorsMiddleware, it would suggest that CORS is only enabled when the REGEX is set, or if signals are used?

    def is_enabled(self, request: HttpRequest) -> bool:
        return bool(
            re.match(conf.CORS_URLS_REGEX, request.path_info)
        ) or self.check_signal(request)

Which goes on to be used within def add_response_headers( and if it's not enabled, it simply returns the normal response.

Does this seem right? I'm not using signals nor regex, just basic configuration for setting the CORS_ALLOWED_ORIGINS.