Problem/Motivation

There's a viable Gadget Chain in \GuzzleHttp\Cookie\FileCookieJar which can be abused to achieve an arbitrary File Write if there's a PHP Object Injection vulnerability in a Drupal application. It cannot be exploited by itself.

(https://portswigger.net/web-security/deserialization#what-is-insecure-de... for general context).

Details of this Gadget Chain have been public for a long time - it looks like it was originally added to PHPGGC around 8 years ago:

https://github.com/ambionics/phpggc/tree/master/gadgetchains/Guzzle/FW/1

I filed an issue to make sure that the Guzzle maintainers are aware but there has been no activity:

https://github.com/guzzle/guzzle/issues/3271 edit - in fact the maintainers addressed this in a different issue: https://github.com/guzzle/guzzle/pull/3334 .

Steps to reproduce

To emphasise again, in order for this to be exploitable within Drupal there first needs to be a PHP Object Injection vulnerability.

https://www.drupal.org/project/gadget_chain_poc/ is a development / security research tool to simulate such a vulnerability.

With that module installed, the guzzle/fw1 gadget chain can be used to write a file to the server (somewhere the PHP user has write permission) which could be used as part of an attack.

I'll leave the full details as an exercise for the reader initially.

Proposed resolution

It doesn't look like the FileCookieJar class is ever used in Drupal core, and a quick search suggests it only appears in contrib as part of the vendored library where that's been committed to projects.

We could remove this class from Drupal's autoloader, and that would render it inert in terms of being exploitable in a PHP Object Injection scenario.

I asked about approaches to doing this in drupal slack and received several interesting suggestions. I'll try to summarise them briefly:

joachim: Have a look at the patch for the composer cache ID thingy - @alexpott worked out how to do it in that with a composer plugin: https://www.drupal.org/project/drupal/issues/3509069

catch: you could add class_alias() very early in a middleware or similar, to point to either your own version, or an empty class.

longwave: look at https://www.drupal.org/node/3509577 as well which messes with the autoloader a bit.. but to block something you might need ... [to throw an exception. Or] you could maybe use reflection to alter Composer's ClassLoader::$missingClasses property to fake that the class doesn't exist, but iirc getting hold of the object to do that is tricky.. or maybe i'm thinking of something else actually

berdir: instead of preventing loading, why don't you ensure the file doesn't exist? similar to core had (has?) workarounds in place to delete certain dev files if they are in the docroot folder with a composer plugin during composer install? ... hooking directly into composer install, you can set that up in composer.json or with a registered plugin ... which is also used to add the scaffold files like robots.txt

chx: a very simple way to do this.. [referring to \Composer\Autoload\ClassLoader::findFile] create an empty file and set apcu to it. it will load it instead of the real class and unlike with class_alias it will not exist after loading it

catch: If we wanted this in core, we could add the file to core-vendor-hardening specifically.

Remaining tasks

* MR
* Tests
* Review / commit etc..
* Release note snippet

User interface changes

n/a

Introduced terminology

n/a

API changes

The class would no longer be available via the autoloader.

Any contrib / custom code that needs to use it would need to load the class explicitly.

Data model changes

n/a

Release notes snippet

This would warrant a CR - that's a todo.

Issue fork drupal-3524971

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

mcdruid created an issue. See original summary.

mcdruid’s picture

In the meantime I've submitted a PR to guzzle with a fairly crude way of minimising the impact of the Gadget Chain:

https://github.com/guzzle/guzzle/pull/3286

We'll see how that goes, but I don't think it's a reason to stop looking at dropping the class from Drupal.

catch’s picture

If the PR solves the issue but is won't fixed, an option would be to fully replace the class in core via adding it to core and then specifying that in the autoloader. I think we have already done that to backport a guzzle security fix in the past. Don't remember which one at the moment.

mcdruid’s picture

The issue and linked PR are going nowhere fast.

I think we should consider replacing the class in Drupal core (per #3), removing it via composer, or removing it from the autoloader if that's feasible.

catch’s picture

I think we could use #3502882: Add a classloader that can handle class moves now to alias it to an empty class somewhere, or hard-code similar behaviour somewhere earlier. Maybe a valid class that throws exceptions in every method or something?

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

mcdruid’s picture

Status: Active » Needs work

The issue I filed with Guzzle has been deleted. The PR hasn't, but it was marked as stale.

I am going to take that as a subtle hint that this is not going to be addressed upstream any time soon.

Using the BackwardsCompatibilityClassLoader seems a really good suggestion.

However, unless I'm missing something it won't work for this in isolation.

It looks like if the old file is still there, that's what will be autoloaded.

I added this to core/core.services.yml under core.moved_classes:

       change_record: https://www.drupal.org/node/3551049
+    'GuzzleHttp\Cookie\FileCookieJar':
+      class: 'Drupal\Component\Cookie\FileCookieJar'
   session.storage.options:

...and put an empty class in place:

$cat core/lib/Drupal/Component/Cookie/FileCookieJar.php 

<?php

namespace Drupal\Component\Cookie;

class FileCookieJar {}

If we then unserialize a bare FileCookieJar, we still get the old class:

$ ddev drush php
Psy Shell v0.12.18 (PHP 8.3.27 — cli) by Justin Hileman

> var_dump(unserialize('O:31:"GuzzleHttp\Cookie\FileCookieJar":0:{}'));
object(GuzzleHttp\Cookie\FileCookieJar)#14083 (4) {
  ["cookies":"GuzzleHttp\Cookie\CookieJar":private]=>
  array(0) {

...snip...

Whereas if I remove the old file, the alias kicks in:

> var_dump(unserialize('O:31:"GuzzleHttp\Cookie\FileCookieJar":0:{}'));
object(Drupal\Component\Cookie\FileCookieJar)#7067 (0) {
}
= null

I'm not sure if that's known / intentional behaviour (happy to file a followup if not) but I'm guessing it's not a problem for the main use of the BackwardsCompatibilityClassLoader.

We could perhaps combine the moved class with one of the earlier suggestions of getting composer to delete the file from vendor on install.

nod_’s picture

Updating search link from the IS: https://search.tresbien.tech/search?q=FileCookieJar There is one module that uses it: tmgmt_smartling

mcdruid’s picture

Issue summary: View changes
mcdruid’s picture

There is one module that uses it: tmgmt_smartling

Thanks. Yes it also looks like some tests use it - it looks like perhaps an old core test did so, and a handful of projects have copies of that old test:

https://git.drupalcode.org/project/drupalladder/-/blob/8.x-1.x/docroot/c...

These all look pretty old, but I've not done a thorough audit of whether any are still in active use.

I still think we could probably remove it by default and provide a way for projects that need it to get it back.. but it's not so clear that "nobody uses this at all".

mcdruid’s picture

Status: Needs work » Needs review

In the new MR I'm trying to use the VendorHardening plugin to remove the file.

We could then provide our own copy somewhere else and alias it.

However, I'm not desperate to fork the file and have to try to keep it in sync with upstream.

We could think about patching it with https://github.com/guzzle/guzzle/pull/3286/changes (which doesn't look like it'll be merged any time soon) instead?

cmlara’s picture

The issue I filed with Guzzle has been deleted. The PR hasn't, but it was marked as stale.

I do see a new PR https://github.com/guzzle/guzzle/pull/3326 that may be addressing the same flaw.

I will note that the new PR specifically includes a note that the Tidelift/SonarSource security team was contacted prior to posting and they agreed with the disclosure, the @mcdruid PR contains no such disclosure.

I am going to take that as a subtle hint that this is not going to be addressed upstream any time soon.

I (obviously) can't see the original issue, was the Tidelift/SonaSource team contacted before posting the issue and was a clearance notice included? Any chance it was just deleted in response to an engineer seeing it, raising issue and proactively being removed for evaluation?

There is one module that uses it: tmgmt_smartling

I had noted in the Slack thread, that this is not a change that can be evaluated by looking just at Drupal Contrib code, one needs to look at the entire dependency stack for all code that may be deployed in the same deployment as Drupal.

Looking at just the Drupal namespace runs a significant risk of false negatives.

The negative outcome of removing the class without providing a replacement can be WSOD's.

mcdruid’s picture

Title: Remove \GuzzleHttp\Cookie\FileCookieJar from the autoloader for security » Defend against \GuzzleHttp\Cookie\FileCookieJar gadget chain
Status: Needs review » Needs work

1) No I didn't contact the guzzle maintainers before filing a public issue / PR, as the details of the gadget chain have been public in the PHPGGC repo for 8+ years.

2) Agree that we cannot conclude that "nobody is using this" on the basis that we don't find (m)any usages in published contrib modules. I think the usage (or lack thereof) of the class is a relevant metric here though.

3) My current thinking on this is that I'd like to look at whether we can patch the class rather than remove it / make it unavailable.

Thanks for pointing out the new PR ( https://github.com/guzzle/guzzle/pull/3326 ). I'll take a look at that and maybe link back to the one I submitted ( https://github.com/guzzle/guzzle/pull/3286 ).

If the guzzle maintainers are happy to make a change that defends against the gadget chain, that'd be great.. and Drupal wouldn't need to do anything else.

If they don't, then I'd like to consider applying our own patch perhaps as part of the composer build; I've not had a chance to dig into the details of how that'd work yet.

mcdruid’s picture

Status: Needs work » Postponed

Apparently the issue I filed for guzzle may have been auto-closed by a bot that tries to catch security issues being reported in public; I shouldn't have assumed that this had been done intentionally.

The good news is that the guzzle maintainers are working on what looks to me like a pretty decent protection against the Gadget Chain:

https://github.com/guzzle/guzzle/pull/3334

Bad news is that it'll be targetted for the next major release.

We could look at a backport patch that we could apply to earlier versions of guzzle (along the lines of my previous comment); the changes are fairly simple.

We should wait until the maintainers commit their fix upstream and then we can decide whether we want to just wait for that to land in Drupal or look at a backport patch.

I'll change this issue to "Postponed" on that basis.

mcdruid’s picture

Status: Postponed » Active

Looks like the hardening was merged and should be in guzzle 8.0

https://github.com/guzzle/guzzle/pull/3334

So Drupal will benefit from that in the long run.

In the meantime, we could look at a backport patch which we'd apply via composer (IIUC).

longwave’s picture

We don't ship composer-patches with core but we could intercept the class by declaring it explicitly up front, so the classloader never tries to load the real one. See #3224421: [PHP 8.1] Add a shim to Guzzle 6 for PHP 8.1 compatibility where we did something similar.

catch’s picture

It looks like 8.0.0 is not that far off if the milestone is accurate: https://github.com/guzzle/guzzle/milestone/17 so we might be able to update to that in main prior to the 12.x release. Intercepting the class until then (and in 11.x and probably 10.6) sounds good.

mcdruid’s picture

New MR based on the suggestions in #17 / #18

I used Claude to help put this together, and I've not yet read through it all carefully - I will do before suggesting it's ready to merge.

I wanted to push this first draft and get the tests to run though.

mcdruid’s picture

Tests are passing - I'll check through everything a bit more thoroughly before marking this as Needs Review.

mcdruid changed the visibility of the branch 3524971-remove-guzzle-filecookiejar to hidden.

mcdruid’s picture

Status: Active » Needs review

I've hopefully removed most of the LLMisms but the test assertions may still be a bit more verbose than needed.

I think this is ready for review.

smustgrave’s picture

Status: Needs review » Needs work

Seems most feedback has been addressed. Could this get a rebase please.

mcdruid’s picture

Status: Needs work » Needs review

Thanks, rebase was a little bit fiddly.

As this is putting a shim into composer.(json|lock) I think it's going to keep hitting the same problem.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

From what I can tell feedback from other reviewers has been addressed for this one, so going to mark.

needs-review-queue-bot’s picture

Status: Reviewed & tested by the community » Needs work
StatusFileSize
new91 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

mcdruid’s picture

The MR's failing because core main (12.x-dev) upgraded to Guzzle 8 on 2026-07-24 (#3612544: Update to Guzzle 8).

So that's great - only remaining question is whether we want to apply this shim to 11.x branches / releases.

longwave’s picture

Version: main » 11.x-dev

Given that we are still supporting Drupal 11 for another two years, then yes I think we should still harden there too.

mcdruid’s picture

Status: Needs work » Reviewed & tested by the community

MR is passing again on 11.x - back to RTBC based on where we were in #26.

  • longwave committed b05412fc on 11.x
    fix: #3524971 Defend against \GuzzleHttp\Cookie\FileCookieJar gadget...
longwave’s picture

Status: Reviewed & tested by the community » Fixed

Because of the composer.json changes this doesn't backport cleanly anywhere else, and I'm a bit hesitant to do this in a patch release although the shim is probably quite safe. As this is a hardening and this has to be chained with another vulnerability to make it exploitable I think that is okay; if anyone disagrees please open backport MRs for other branches.

Committed and pushed b05412fc21c to 11.x. Thanks!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

mcdruid’s picture

Issue summary: View changes

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.