Problem/Motivation

\Drupal\Core\EventSubscriber\FinishResponseSubscriber::setResponseCacheable()
guards the Vary: Cookie append with:

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

The intent is "don't stomp existing Vary values." The implementation is
"if anyone has set Vary, skip Cookie entirely." When any other
subscriber has already added a Vary value, hasVary() returns
TRUE and Cookie is silently dropped.

The most common trigger in the wild is asm89/stack-cors. When
cors.config is configured with multiple allowedOrigins
(and typically supportsCredentials: true),
CorsService::configureAllowedOrigin() unconditionally calls
varyHeader($response, 'Origin') on every response — even
non-CORS requests. That is spec-correct (the ACAO header echoes the
requester's Origin, so caches must Vary on it), but it trips Drupal's
guard.

Impact: anonymous cacheable responses (including 403s
on /admin, /node/<id>/edit, and any other
access-denied path) are emitted as
Cache-Control: public, max-age=<cache.page.max_age> with a
Vary that does not include Cookie. Shared caches (Varnish, Akamai,
Cloudfront, etc.) have no signal to segment by session and will serve the
anonymous body to authenticated users. This is an auth-bypass hazard for
any site using multi-origin CORS behind a shared cache.

Related history:

  • #3128982
    asm89/stack-cors 1.x → 2.x upgrade. Only fixes wildcard
    and single-origin configs; the multi-origin branch still adds
    Vary: Origin unconditionally, by spec design.
  • #3001809
    — closed as duplicate of #3128982; the guard interaction was raised
    in comments but never patched.

Steps to reproduce

  1. Configure cors.config in services.yml with
    two or more allowedOrigins and
    supportsCredentials: true. Example:
    parameters:                                    
        cors.config:                                                                                                                                                  
          enabled: true                       
          allowedHeaders: ['*']                                                                                                                                       
          allowedMethods: ['*']                                                                                                                                       
          allowedOrigins:
            - 'https://example.com'                                                                                                                                   
            - 'https://preview.example.com'                     
          supportsCredentials: true
  2. Rebuild the container.
  3. As an anonymous user, request any 403 route. E.g.
    curl -sSI https://example.com/admin.
  4. Observe the response headers:
    HTTP/1.1 403 Forbidden                                                                                                                               
      Cache-Control: max-age=43200, public        
      Vary: Origin

    Cookie is absent from Vary. Any shared cache
    in front of the origin will now serve this 403 to authenticated users on
    subsequent requests to the same URL.

Proposed resolution

Check specifically whether Cookie is already present in the
Vary header, instead of gating on any Vary being set:

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

  // After                                                                                                                                                        
  if (!in_array('Cookie', $response->getVary(), TRUE) && !Settings::get('omit_vary_cookie')) {
    $response->setVary('Cookie', FALSE);                                                                                                                          
  }

This preserves the original intent (don't duplicate an existing
Cookie value; don't touch responses where the site has opted
out via omit_vary_cookie) while ensuring Cookie is present
whenever any other subscriber has already added an unrelated Vary value.

Remaining tasks

  • Review patch.
  • Add a unit/kernel test asserting that Cookie ends up in
    Vary after setResponseCacheable() runs on a
    response that already has Vary: Origin.
  • Confirm the change doesn't regress the omit_vary_cookie
    opt-out path.
  • Commit.

User interface changes

None.

Introduced terminology

None.

API changes

None. The change is confined to the internal Vary-header composition
logic in FinishResponseSubscriber. Behaviour changes only when
a response already has a non-Cookie Vary value and
omit_vary_cookie is not set — in which case Cookie
is now correctly appended.

Data model changes

None.

Release notes snippet

Anonymous cacheable responses now correctly include Cookie
in their Vary header even when other subscribers (such as the
CORS middleware in multi-origin configurations) have already added an
unrelated Vary value. Previously, Vary: Cookie was silently
dropped in that case, which could cause shared HTTP caches to serve
anonymous responses to authenticated users.

CommentFileSizeAuthor
#3 3612557-1.patch2.29 KBjhedstrom

Issue fork drupal-3612557

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:

Comments

jhedstrom created an issue. See original summary.

jhedstrom’s picture

Status: Active » Needs review
StatusFileSize
new2.29 KB

Adding a patch for use with composer.

smustgrave’s picture

Version: 11.4.x-dev » main
Status: Needs review » Needs work

THanks for reporting! Could you update the MR for main please. We can backport from there.