nelmio / NelmioCorsBundle

Adds CORS (Cross-Origin Resource Sharing) headers support in your Symfony application
https://symfony.com/bundles/NelmioCorsBundle/
MIT License
1.89k stars 108 forks source link

Controller action gets executed for invalid origins #157

Closed shagenbrock closed 1 year ago

shagenbrock commented 3 years ago

Hi,

we have an issue with https post requests in chrome that controller actions beeing executed even if the origin is rejected.

Q A
Affected Bundle Versions ^1.3.2, ^2
Affected Symfony Versions checked with ^2.8, ^4.4
Affected Browsers Chrome (86.0.4240.111), Safari (Version 14.0 (15610.1.28.1.9, 15610))
Not Affected Firefox (81.0.2)

Example CORS configuration for a symfony application under "api.example.com"

nelmio_cors:
    paths:
        '^/api/':
            allow_origin:
              - 'https://valid-origin-host.example.com'
            allow_headers: ['Content-Type', ...]

Under a seperate domain (e.g.: https://invalid-origin-host.example.com) we have a file with following content:

<html>
<head>
<script src="https://code.jquery.com/jquery-3.5.1.min.js" crossorigin="anonymous"></script><script>
$( document ).ready(function() {
    $.ajax({
        url: "https://api.example.com/api/example-endpoint",
        method: "POST",
        xhrFields: {withCredentials: true},
        data: {"title":"example payload"},
        crossDomain: true
    });
});
</script>
</head>
<body>
</body>
</html>

If you open the script you will see a invalid cors message in the browser console and the response of the endpoint would not be accessible for the javascript. Nevertheless the main controller action gets executed on the server as the request is not stopped with invalid origins.

Note: There was a forced response in this case that got removed in version 1.3.2.

Seldaek commented 3 years ago

Can you share what you see in the network tab & console exactly?

Theoretically it should do an OPTIONS request, which will return a deny response, and then your controller would not be reached.

shagenbrock commented 3 years ago

Hi,

there is no preflight options requests, that is whats irritates me. Its directly a post request at this point. Chrome did indead deny the request with the corresponding "CORS" message. But the controller gets executed anyway.

Access to XMLHttpRequest at 'https://api.example.local/api/message' from origin 'https://blocked-host.example.local' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

curl -X "POST" "https://api.example.local/api/message" \
     -H 'Host: api.example.local' \
     -H 'Connection: keep-alive' \
     -H 'Pragma: no-cache' \
     -H 'Cache-Control: no-cache' \
     -H 'Accept: */*' \
     -H 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36' \
     -H 'Content-Type: application/x-www-form-urlencoded; charset=UTF-8' \
     -H 'Origin: https://blocked-host.example.local' \
     -H 'Sec-Fetch-Site: same-site' \
     -H 'Sec-Fetch-Mode: cors' \
     -H 'Sec-Fetch-Dest: empty' \
     -H 'Referer: https://blocked-host.example.local/' \
     -H 'Accept-Encoding: gzip, deflate, br' \
     -H 'Accept-Language: en,de-DE;q=0.9,de;q=0.8,en-US;q=0.7' \
     -H 'Cookie: sid=sid-value;' \
     --data-urlencode "body=Test nelmio 1.5.6 T10" \
     --data-urlencode "recipients=1010" \
     --data-urlencode "recipients_external="

I have added a .har file with the current requests. Hope that helps.

modified_cors_issue.har.zip

ServerExe commented 1 month ago

Sorry to open this topic again but it is indeed a security issue when API endpoints are still executing logic even though there was a CORS policy block (without preflight).

Reason seems to be backwards-compatibility to older browsers or something. There is a nice website explaining this exact situation.

We also faced this issue with a POST endpoint, which subscribes logged in users to our webinar events via AJAX by reading a httpOnly secured cookie session value. What currently prevents it is that our cookie is set to samesite=lax. The only solution here is to additionally transfer a custom x-header to your API. Only then, browser are doing an OPTIONS preflight request. Tested on Chrome.

But how to prevent that in the symfony application in case somebody is not requesting with the custom header? Probably, a solution would be a CompilerPass in this bundle, which subscribes to the kernel request and always requires a custom header X-SYMFONY-CORS header (or whatever name) to always be transferred. In case it's not set, returns corresponding response.

But this solution doesn't work for GET requests which are called in the iFrames, for instance. You can't transmit this header. So, very confusing topic :D