Problem/Motivation

If you use ajax requests from the same origin, CORS support is omitted (for obvious reasons) and no `Origin` key is added to the `Vary` header and naturally the Access-Control-Allow-Origin header is not emitted. However, the request does cache and if a request from another origin is made, it receives the cached item without the CORS data.

Proposed resolution

Technically, every route in Drupal is a CORS route since CORS will activate if an Origin header is passed in the request. So shouldn't the Origin key be added to the Vary response for every Drupal request? That way, upstream caches will variate their cache and miss if the origin header is present or different?

CommentFileSizeAuthor
#66 drupal-n3001809-66-100x.patch4.87 KBDamienMcKenna
#56 interdiff_52-56.txt3.51 KBravi.shankar
#56 3001809-56.patch4.58 KBravi.shankar
#52 interdiff_42-52.txt3.53 KBravi.shankar
#52 3001809-52.patch4.51 KBravi.shankar
#42 interdiff_33-42.txt1.24 KBravi.shankar
#42 3001809-42.patch4.53 KBravi.shankar
#33 3001809-32-cors-origin.patch4.69 KBmr.baileys
#33 interdiff.txt3.35 KBmr.baileys
#32 interdiff.txt921 bytesmr.baileys
#32 3001809-32-cors-origin.patch4.49 KBmr.baileys
#30 3001809-30-cors-origin.patch4.49 KBmr.baileys
#26 3001809-always-vary-by-origin-26.patch847 bytesoknate
#23 3001809-23.patch6.35 KBsebas5384
#22 3001809-22.patch5.42 KBsebas5384
#21 3001809-21.patch5.41 KBsebas5384
#12 3001809-12.patch4.76 KBWim Leers
#12 interdiff.txt2.07 KBWim Leers
#11 3001809-11.patch3.14 KBWim Leers
#11 interdiff.txt972 bytesWim Leers
#10 3001809-10.patch3.13 KBWim Leers
#10 interdiff.txt3.06 KBWim Leers
#7 3001809-8.patch2.38 KBWim Leers
#7 interdiff.txt1.58 KBWim Leers
#5 3001809-5.patch1.4 KBWim Leers

Issue fork drupal-3001809

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Josh Waihi created an issue. See original summary.

znerol’s picture

So shouldn't the Origin key be added to the Vary response for every Drupal request? That way, upstream caches will variate their cache and miss if the origin header is present or different?

Yes. The Vary response header must be identical on every response from the same route. Otherwise this will lead to problems with caching proxies (and even the browser cache). This is why Vary: Cookie header is added unconditionally to every response - even on anonymous requests.

Looks like the affected code is located in asm89/stack-cors, thus this needs to be fixed over there.

Wim Leers’s picture

Component: cache system » request processing system
Issue tags: +API-First Initiative, +D8 cacheability

Great find!

Interestingly, asm89/stack-cors does have test coverage for doing exactly this: https://github.com/asm89/stack-cors/blob/c163e2b614550aedcf71165db2473d9.... The problem is that it does so conditionally: it only does this for request that are:

  • CORS requests
  • not preflight requests
  • for requests that have an allowed Origin header

And that conditionality is exactly what is the problem here of course: the Vary header must always be set. Nobody reported this yet at https://github.com/asm89/stack-cors/issues?utf8=%E2%9C%93&q=vary.

I guess the assumption that asm89/stack-cors makes is that any request that contains a Origin header is not allowed to be served a response from a reverse proxy (such as Drupal's built-in page_cache and dynamic_page_cache). I wonder what Varnish does? Based on https://stackoverflow.com/questions/43998474/varnish-caching-options-cor..., it also doesn't handle this automatically?

P.S.: all AJAX requests that Drupal makes are POST requests, which are never cached.

Josh Waihi’s picture

Looks like the affected code is located in asm89/stack-cors, thus this needs to be fixed over there.

But Drupal makes the decision that all routes could be CORS responses, so isn't it Drupal's job to provide the Origin key in the Vary header? As I'd imagine not all applications that use asm89/stack-cors require all responses in there application to be CORS ready?

P.S.: all AJAX requests that Drupal makes are POST requests, which are never cached.

Ugh. Unfortunately true.

Wim Leers’s picture

Status: Active » Needs review
FileSize
1.4 KB

The laravel-cors middleware (which also reuses asm89/stack-cors) had a similar discussion >4 years ago in https://github.com/barryvdh/laravel-cors/issues/20. https://github.com/barryvdh/laravel-cors/issues/20#issuecomment-50369259 says exactly what you say in #4. That led to https://github.com/asm89/stack-cors/pull/11, which we (Drupal) helped land.

As I'd imagine not all applications that use asm89/stack-cors require all responses in there application to be CORS ready?

Right — Drupal doesn't have this luxury because it needs routing to happen before it can even know whether a particular URL will need to vary by Origin.

Which brings me to the same conclusion as you:

But Drupal makes the decision that all routes could be CORS responses, so isn't it Drupal's job to provide the Origin key in the Vary header?

I think you're right.

But this is a pretty big change. In principle, certain responses may also be available to anonymous users. Which means that Drupal core's Page Cache would also need to vary by Origin (because certain origins may need to get a 403). Which would make it far less effective.


Before we make any changes to core, let's add a failing test that demonstrates the problem.

Wim Leers’s picture

Note: if this is correct:

But this is a pretty big change. In principle, certain responses may also be available to anonymous users. Which means that Drupal core's Page Cache would also need to vary by Origin (because certain origins may need to get a 403). Which would make it far less effective.

Then we'll need changes like this:

diff --git a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
index e99caa1..e82f929 100644
--- a/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
+++ b/core/modules/dynamic_page_cache/src/EventSubscriber/DynamicPageCacheSubscriber.php
@@ -84,6 +84,9 @@ class DynamicPageCacheSubscriber implements EventSubscriberInterface {
         // requested that the subscriber does not support.
         // @see \Drupal\Core\EventSubscriber\RenderArrayNonHtmlSubscriber::onResponse()
         'request_format',
+        // Vary by CORS response in case that is turned on.
+        // @todo only do this if the `cors.config` container parameter has `enabled` set to `true`.
+        'headers:Origin',
       ],
       'bin' => 'dynamic_page_cache',
     ],
diff --git a/core/modules/page_cache/src/StackMiddleware/PageCache.php b/core/modules/page_cache/src/StackMiddleware/PageCache.php
index ca9a733..9bb5905 100644
--- a/core/modules/page_cache/src/StackMiddleware/PageCache.php
+++ b/core/modules/page_cache/src/StackMiddleware/PageCache.php
@@ -342,6 +342,9 @@ protected function getCacheId(Request $request) {
     $cid_parts = [
       $request->getSchemeAndHttpHost() . $request->getRequestUri(),
       $request->getRequestFormat(),
+      // Vary by CORS response in case that is turned on.
+      // @todo only do this if the `cors.config` container parameter has `enabled` set to `true`.
+      $request->headers->get('Origin'),
     ];
     return implode(':', $cid_parts);
   }
Wim Leers’s picture

#5 passes for me locally. The existing test coverage in HEAD proves that CORS runs after Page Cache:

    // Fire the same exact request. This time it should be cached.
    $this->drupalGet('/test-page', [], ['Origin' => 'http://example.com']);
    $this->assertSession()->statusCodeEquals(200);
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://example.com');

    // Fire a request for a different origin. Verify the CORS header.
    $this->drupalGet('/test-page', [], ['Origin' => 'http://example.org']);
    $this->assertSession()->statusCodeEquals(200);
    $this->assertSession()->responseHeaderEquals('X-Drupal-Cache', 'HIT');
    $this->assertSession()->responseHeaderEquals('Access-Control-Allow-Origin', 'http://example.org');

Note how both are a cache hit as far as Page Cache is concerned, yet they have different response headers.

This means that also a 403 due to CORS reasons should still be able to happen. Let's expand the test coverage to prove this.

Wim Leers’s picture

D'oh, ignore #7, that test coverage already existed a bit further down 😅

Status: Needs review » Needs work

The last submitted patch, 7: 3001809-8.patch, failed testing. View results

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
3.06 KB
3.13 KB

#5 set the foundation in that it adds a request to the same URL without specifying the Origin request header.

This patch then adds assertions for the Vary header that match the current behavior.

Wim Leers’s picture

+++ b/core/tests/Drupal/FunctionalTests/HttpKernel/CorsIntegrationTest.php
@@ -39,28 +39,31 @@ public function testCrossSiteRequest() {
     // Fire off a request without 'Origin' request header.
     $this->drupalGet('/test-page');
     $this->assertSession()->statusCodeEquals(200);
+    $this->assertSession()->responseHeaderEquals('Vary', '');

This is how it works today, but this is wrong. A single request without an Origin header would prime the reverse proxy's cache (and Drupal core's Page Cache, which is just a simple built-in reverse proxy) and result in incorrect responses.

This will fail. 🔥

Wim Leers’s picture

My analysis in #6 is wrong, because of the reasons stated in #7: the CORS middleware runs after the Page Cache middleware.

This is also why Drupal core is not affected by this bug: because the CORS middleware (priority 250) wraps the Page Cache middleware (priority 200). This was done in #2850034: CORS allow-origin '*' not possible because of cached headers without thinking through the consequences for reverse proxies.


If we're to follow this pattern, then the recommendation would be for reverse proxies wrapping Drupal to do CORS themselves, and not use Drupal's native support for this.

That would of course require infrastructure-level configurability that doesn't exist everywhere.


So the simplest alternative would be to indeed add the Vary: Origin response header to all responses, until https://github.com/asm89/stack-cors/issues/49 (just opened that) gets solved.

Wim Leers’s picture

+++ b/core/tests/Drupal/FunctionalTests/HttpKernel/CorsIntegrationTest.php
@@ -21,6 +21,18 @@ class CorsIntegrationTest extends BrowserTestBase {
+  protected function setUp() {
+    parent::setUp();
+
+    // Enable page caching.
+    $config = $this->config('system.performance');
+    $config->set('cache.page.max_age', 3600);
+    $config->save();
+  }

Without this, #12 will still fail because \Drupal\Core\EventSubscriber\FinishResponseSubscriber::onRespond() ends up calling \Drupal\Core\EventSubscriber\FinishResponseSubscriber::setResponseNotCacheable(), which does $response->setVary(NULL).

Why is that called? Because by default, cache.page.max_age === 0, which means that no responses from Drupal are allowed to be cached by any intermediary (such as a reverse proxy), and hence it's okay to throw away Vary.

So this allows intermediary caching, and in doing so it also allows us to test Vary sanely.

The last submitted patch, 10: 3001809-10.patch, failed testing. View results

The last submitted patch, 11: 3001809-11.patch, failed testing. View results

Status: Needs review » Needs work

The last submitted patch, 12: 3001809-12.patch, failed testing. View results

Wim Leers’s picture

Behat\Mink\Exception\ExpectationException: Current response header "Vary" is "Origin,Accept-Encoding", but "Origin" expected.

I forgot that DrupalCI's test env somehow always adds Accept-Encoding but local test runner don't 🙃

sebas5384’s picture

Hi,

I can confirm this issue because on my case I'm having multiple clients requesting the same (API) so when te request is from the server which doesn't has any Origin header then when the browser requests it's missing the Access-Control-Allow-Origin .

Btw, thanks for this issue! what's missing for this patch to be RTBC ?

sebas5384’s picture

Hey @Wim Leers ! after I applied the #12 patch the Cookie value was removed from the Origin header, do you have any ideia of why?

thanks!!

Wim Leers’s picture

#19: no, no idea :( But PageCacheTest is failing for a similar reason, so that problem you're observing must also be causing at least that test to fail. It'd be great if you could debug this and update the patch… 🙏 😊

sebas5384’s picture

Hey @Wim Leers, I just changed the order since this code at the setResponseCacheable method:

if (!$response->hasVary() && !Settings::get('omit_vary_cookie')) {
  $response->setVary('Cookie', FALSE);
}

is not adding the Cookie in case there's already content in the Vary header, but since I don't know what's the reason of this I just changed the order to add the Origin after the method execution, attached there's a rerolled patch and also I added the cookie value to our tests.

I hope this make's sense :P

sebas5384’s picture

Trying to pass the tests now.

sebas5384’s picture

This patch it's for the 8.6.x

tjwelde’s picture

We port a site to headless currently and are using GraphQL requests, with APQ (which optimises the queries), which means that we can send those requests as GET and cache them.
We also do server-side-rendering, so the GraphQL requests come either from a server, or a client.

A big issue was, that Acquias Varnish proxy threw away access-control-allow-origin headers, when being requested without origin header. We could circumvent it temporarily, by setting an origin header on our server request, but the underlying issue remained.
Anyone knowing the issue could potentially easily exploit it and make the site unusable for everyone (since without cors header, no data could be requested by clients).

This patch fixed this issue.
Thanks for the effort!

Version: 8.7.x-dev » 8.8.x-dev

Drupal 8.7.0-alpha1 will be released the week of March 11, 2019, which means new developments and disruptive changes should now be targeted against the 8.8.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

oknate’s picture

Reroll for 8.7, dropping tests, as I don't have time to investigate them right now.

Anonymous’s picture

After applying the patch in #26, I'm still getting an issue with 403 errors on AJAX'd request with an explicit `allowedOrigin` set.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.0-alpha1 will be released the week of October 14th, 2019, which means new developments and disruptive changes should now be targeted against the 8.9.x-dev branch. (Any changes to 8.9.x will also be committed to 9.0.x in preparation for Drupal 9’s release, but some changes like significant feature additions will be deferred to 9.1.x.). For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

mr.baileys’s picture

I created a pull request against asm89/stack-cors to (optionally) always output the Vary: Origin header.

mr.baileys’s picture

Status: Needs work » Needs review
FileSize
4.49 KB

Attempt to reroll #23 against 8.9.x, no other changes (although there were some conflicts during rebase).

Status: Needs review » Needs work

The last submitted patch, 30: 3001809-30-cors-origin.patch, failed testing. View results

mr.baileys’s picture

Status: Needs work » Needs review
FileSize
4.49 KB
921 bytes

Seems the only test failure is caused by the order in which the headers in the vary-header are listed. Rather than checking the full vary-header string, we should just check for the presence of the 'origin' header in it (since we want to verify that 'Origin' is added to the 'Vary'-header), but for now let's see if we can get the tests to pass by just changing the order.

mr.baileys’s picture

Took me way too long to figure out that running drush core tests with nginx is not the best idea.

Switched the test to just verifying the presence of Origin in the Vary-header. Interdiff is against #30, not #32. Passes locally.

The last submitted patch, 32: 3001809-32-cors-origin.patch, failed testing. View results

Wim Leers’s picture

@oknate Want to review this? :)

Lukas von Blarer’s picture

The patch in #33 worked for me to resolve caching issues with varnish.

davidwbarratt’s picture

This has been fixed upstream. Updated the library in #3128982: Upgrade asm89/stack-cors to ^2.0 to fix cacheability

Version: 8.9.x-dev » 9.1.x-dev

Drupal 8.9.0-beta1 was released on March 20, 2020. 8.9.x is the final, long-term support (LTS) minor release of Drupal 8, which means new developments and disruptive changes should now be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

andypost’s picture

  1. +++ b/core/lib/Drupal/Core/EventSubscriber/FinishResponseSubscriber.php
    @@ -175,6 +175,11 @@ public function onRespond(FilterResponseEvent $event) {
    +    // @todo Remove this when https://github.com/asm89/stack-cors/issues/49 lands and Drupal core updates to a new release.
    

    The issue fixed

  2. +++ b/core/tests/Drupal/FunctionalTests/HttpKernel/CorsIntegrationTest.php
    @@ -21,6 +21,18 @@ class CorsIntegrationTest extends BrowserTestBase {
    +  protected function setUp() {
    

    need void type to return

davidwbarratt’s picture

andypost, can you take a look at #3128982: Upgrade asm89/stack-cors to ^2.0 to fix cacheability? That fixes this issue.

andypost’s picture

Thank you, upgrade to 2.0 helps, guess it could be closed as duplicate

ravi.shankar’s picture

Status: Needs work » Needs review
FileSize
4.53 KB
1.24 KB

I have rerolled patch #33 and addressed comment #39.

Status: Needs review » Needs work

The last submitted patch, 42: 3001809-42.patch, failed testing. View results

davidwbarratt’s picture

If #3128982: Upgrade asm89/stack-cors to ^2.0 to fix cacheability is resovled, I'm not sure why this patch would be needed any longer?

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

andypost’s picture

erin.rasmussen’s picture

It makes sense to me that the fix in #3128982: Upgrade asm89/stack-cors to ^2.0 to fix cacheability will be released as a part of d10. We don't want to introduce big changes in a minor release. Is there a way to update Drupal 9 to use asm89/stack-cors to ^2.0 ?
What would be the best way to apply to Drupal 9 for testing?

DamienMcKenna’s picture

Issue tags: +Pantheon

Tagging this "Pantheon" because it seems to help CORS errors that are logged when running a site on Pantheon.

mstrelan’s picture

Is there a way to update Drupal 9 to use asm89/stack-cors to ^2.0 ?

@erin.rasmussen you can alias the package version.

composer require "asm89/stack-cors:2.0.5 as 1.3.0"

ravi.shankar’s picture

Fixed failed test of patch #42.

andypost’s picture

Status: Needs review » Needs work

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

useernamee’s picture

I've ran into this issue only recently on drupal 9.3.11 and varnish-5.2.1 (on amazeeio hosting). After applying #52 patch the issue was solved. If I add some more context we are having a decoupled website and the request to rest menu items was failing due to missing access-control-allow-origin header. So thanks to everyone working on this issue. I'd say it can go into RTBC after tests are passing

ravi.shankar’s picture

Fixed failed tests of patch #52.

useernamee’s picture

Status: Needs work » Reviewed & tested by the community

Great @ravi.shankar, I'm moving the ticket to RTBC.

The last submitted patch, 52: 3001809-52.patch, failed testing. View results

The last submitted patch, 52: 3001809-52.patch, failed testing. View results

catch’s picture

Status: Reviewed & tested by the community » Closed (duplicate)

Marking this duplicate of #3128982: Upgrade asm89/stack-cors to ^2.0 to fix cacheability. Sites can alias the cors version for Drupal 9. If that's not enough, then we could look at backporting the cors update after all but I don't think we should workaround it.

Dave Reid’s picture

#3128982: Upgrade asm89/stack-cors to ^2.0 to fix cacheability is likely not going to be backported to D9.5 though, given this is a bug, we should probably still have this open for resolving here?

Dave Reid’s picture

Status: Closed (duplicate) » Needs work

Given folks can use a composer alias, the consensus was just to leave this as 10.x only

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

dpagini’s picture

Version: 11.x-dev » 9.5.x-dev

When reading through this issue, I think this should ONLY be tagged for branch 9.5.x, which is about to (3 months) be marked unsupported, potentially killing this issue. But yeah, I don't think this is relevant to 10.x or 11.x at all, since they are using the new 2.x asm89/stack-cors library. Does anyone disagree with that?

DamienMcKenna’s picture

Version: 9.5.x-dev » 11.x-dev
Status: Needs work » Needs review
FileSize
4.87 KB

Rerolled for 10.0.x that also applies to 11.x.

smustgrave’s picture

Status: Needs review » Needs work

Have not reviewed but seems to have a test failure.

kunal.sachdev made their first commit to this issue’s fork.