Followup for Drupal CORE SA 2022-012

This issue was previously raised with the Drupal Security Team in SDO 1715391 NOTE: This is a private security tracker, only members of the Security Team and individuals who have been added will be able to access this link. Do not report Access Denied errors.

This solution was deferred in order to avoid route changes and cache refreshes in a Security Release.

The changes suggested in this modify routes and constructors which are NOT considered API by the Drupal compatibility policy and as such these changes should be eligible for resolution in existing branches, see Drupal 8 and 9 backwards compatibility and internal API policy. This issue also resolves a security issue and should be considered for the security-only branch of 9.3.x

Problem/Motivation

Drupal's ImageStyleDownloadController will serve content for schemes it is not responsible for as long as a valid itok is provided. For a core only deployment the only risk is increased server load. With contrib modules involved this can lead to an abuse of resources (DoS) and an Access Control bypass.

It is inefficient, fragile, and ultimately insecure to attempt to block this from contrib as it relies on detecting what routes that trace their heritage to ImageStyleDownloadController and register additional and unnecessary 'null' routes into the Drupal routing system to deny access. Additionally patching in Core will provide a more secure example for future contrib modules to model after.

Steps to reproduce

Upload an image to content (such as an Article) that will generate an ImageStyle derivative.
Assume the file is named public://2021-09/2020-11-20 18.09.12-1.jpg

Obtain the link to the ImageDerivative through the image.style_public route. Assuming this is for a 'large' derivative the link may be "sites/default/files/styles/large/public/2021-09/2020-11-20 18.09.12-1.jpg?itok=JzmJlrXt"

Attempt to access the file via the image.style_private route controller (assuming the example above) "system/files/styles/large/public/2021-09/2020-11-20 18.09.12-1.jpg?itok=JzmJlrXt"

Expected Result:
The content is not served
Current Result:
Drupal Streams the content to the requestor.

Proposed resolution

Prevent the routes from severing content for schemes they do not manage.

Remaining tasks

User interface changes

None

API changes

Non-api constructor changes.
Note: While the classes involved are NOT API we are aware of classes that extend these which could cause issues for contrib.

Data model changes

None

Release notes snippet

For security hardening, a backwards-compatibility break has been introduced in ImageStyleDownloadController. This change may affect modules that provide custom stream wrappers or extend ImageStyleDownloadController. Review the change record for information on how to update your routing entries for this change.

Issue fork drupal-3298701

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

cmlara created an issue. See original summary.

cmlara’s picture

Title: ImageStyleDownloadController routes do not limit schems served. » ImageStyleDownloadController routes do not limit schemes served
Status: Active » Needs work
Issue tags: +Needs tests
StatusFileSize
new3.25 KB

Patch originally presented by @effulgentsia, modified to latest 9.4.x branch.

cmlara’s picture

Adding the test, original credit to @effulgentsia for the tests, modified by myself to handle Core SA 2022-012 related changes.

Fail patch is equivalent to an interdiff.

Adding the Contrib Blocker tag as the currently known method to work-around this requires contrib streamWrapeprs to know the scheme's they will be serving during the route building stage. S3fs in its 4.x branch currently as drafted will not know about schemes until much later (uses ConfigEntities which are not available during the route building stage). Per priority list this also moves to Major for not having a workaround.

cmlara’s picture

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

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should 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.

driskell’s picture

Just been looking at https://www.drupal.org/project/drupal/issues/3058071 - which is similar - at moment it is always public for all streams that aren't the "private" one, so maybe that's a separate issue there as the advisory fixed only the Image style controller.

And it looks like in this patch there becomes a strange behaviour:
* With image derivatives turned on, it properly obeys the route "public" setting and only serves the correct stream
* With image derivatives turned off, it will ignore the route "public" setting, and instead check the additional public streams setting

Should this patch instead use the "public" setting from the route in both cases? Perhaps either deprecating the additional public streams setting in the process, or if that is needed for API compatibility, having the ability to detect "no public setting added" and obey the additional public streams?

cmlara’s picture

Looking closer, I actually think I should split this up and pull out the $public portion.

Its a good change for the future to allow re-use of the class to have the $public option, but it isn't strictly needed to accomplish the goal of just limiting the controller to only respond to specific schemes.

On quick glance I do agree with you that the mixed use probably is not correct (and that is on me for how I ported previous work by @effulgentsia to fit in to current branches)

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative

Brought this up in slack in the #needs-review-queue-initiative channel and @cmlara talked about rescoping, #8

cmlara’s picture

Version: 9.5.x-dev » 10.1.x-dev
Priority: Major » Normal
Issue tags: -Contributed project blocker

Downgrading priority as we found a work-around to add override routes no longer making this a contrib blocker (though it is still considered unreliable and could lead to security issues occurring)

Moving issue to 10.1.x and can be backported.

cmlara’s picture

StatusFileSize
new6.75 KB
new2.4 KB

Rebased on 10.1.x

Removed the $public parameter per #8

cmlara’s picture

Status: Needs work » Needs review

smustgrave’s picture

+1 from me but will this need a change record now? If not think it can be marked RTBC.

cmlara’s picture

cmlara’s picture

One more minor change, adding string parameter typehint to the new parameter and removing the NULL default.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for the quick change record!

catch’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
@@ -140,6 +142,10 @@ public function deliver(Request $request, $scheme, ImageStyleInterface $image_st
 
+    if ($required_derivative_scheme !== $derivative_scheme) {
+      throw new AccessDeniedHttpException();
+    }

I think we should add an exception message here explaining why access is denied. If someone gets here for non-nefarious reasons it would help them to work out why.

rishabh vishwakarma’s picture

Status: Needs work » Needs review
StatusFileSize
new6.79 KB
new634 bytes

Addressed the suggestion from #18

smustgrave’s picture

Status: Needs review » Needs work

Think we want a different type of error message.

Brought this up in slack #needs-review-queue-initative channel

From @catch

Was thinking more like the scheme for the image style doesn't match the scheme for the original image

From @cmlara

I’ll note that derivatives can be on a different scheme than the source image (read-only streamWrappers)
Any message we put on there really is for DX IMHO, as image styles links should always be built by buildUrl() this means only developer error or probing from a user should ever trigger it.

tanuj.’s picture

Status: Needs work » Needs review
StatusFileSize
new6.82 KB
new709 bytes

Changing exception message as per suggestions from #20.

Status: Needs review » Needs work

The last submitted patch, 21: 3298701-21.patch, failed testing. View results

cmlara’s picture

Status: Needs work » Needs review

Random test failure, retest is green, back to NR.

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Think the message is better now.

Though since changing it didn't break anything I wonder if we should add an assertion testing for the message. Will let the committer decide if that's overkill or not.

larowlan’s picture

Crediting @effulgentia

larowlan’s picture

I've pinged security team folks in slack to ask some questions about this

larowlan’s picture

+++ b/core/modules/image/src/Controller/ImageStyleDownloadController.php
@@ -106,7 +108,7 @@ public static function create(ContainerInterface $container) {
+  public function deliver(Request $request, $scheme, ImageStyleInterface $image_style, string $required_derivative_scheme) {

Should we make this optional for now, defaulting to e.g. public - is a BC break otherwise

Crediting @benjifisher from the security team for pointing out the above

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
cmlara’s picture

Should we make this optional for now, defaulting to e.g. public - is a BC break otherwise

As noted in the IS, this class is @internal (by default) and not subject to the BC policy. See https://www.drupal.org/about/core/policies/core-change-policies/bc-polic....

I don't believe adding a default of 'public' is likely to fix much and instead will just allow a security hole to persist.

With a default any class extending the base controller class, or registering a route utilizing the controller would still serve public:// scheme bypassing any new rules placed when a module replaces the existing controller (like s3fs public takeover).

Additionally a default would still prevent 3rd party routes from serving content because their scheme wouldn't match the 'public' scheme for the destination image still rendering the route broken.

If we are adding a default to prevent a WSOD I think the best we can do is make the value nullable with a NULL default allowing the 403 to be thrown for scheme mismatch, or if we really want to go the extra mile a 403 with a DX targeted make the 403 message referencing the change notice.

EDIT: Link to BC policy was missing a '/'

benjifisher’s picture

@cmlara:

The link in #31 is missing a '/' in 'aboutcore'. If I add that and scroll up the page, I see the paragraph,

The Drupal core deprecation policy applies to both public and internal APIs: wherever possible, old APIs should be deprecated for removal in the next major version instead of being removed immediately. For public APIs, the deprecation process is a requirement; for internal APIs, we provide BC and deprecation where possible, but breaking changes may be made in situations where BC is not possible or has negative consequences. (Even for internal APIs, core contributors should always try to follow the deprecation process first, or document in issue discussions why deprecation is not used.)

As you say, the BC layer is not a requirement in this case, but I think the "where possible" part applies.

One thing that bothers me about this issue is that I do not see any discussion of whether this is the simplest/safest approach. For example, could the page controller figure out the expected scheme without being given an extra parameter? Maybe not, but I would like to document that here on the issue.

cmlara’s picture

Note: in my mind a BC layer is 'not possible' if it retains the vulnerability. This is in line with Drupal being a 'secure' cms and fixing vulnerabilities before they can be exploited. If a core committer wants a layer that retains the vulnerability I believe it necessitates an explicit sign-off to do so.

One thing that bothers me about this issue is that I do not see any discussion of whether this is the simplest/safest approach. For example, could the page controller figure out the expected scheme without being given an extra parameter?

I don't think we can get much more simple or secure than the patch as it exists.

The scheme we receive as part of the request does not reflect the routes intended usage and is fully in the hands of an attacker, as such we can not trust that data. We can not depend upon the route name (if we were to do a request to route name lookup) because there is no hard coded rule that the route name must be a specific format. If we were to implement such a check we may be able to make it work for core (because we know the routes) but still break contrib. See last suggestion in this post for relevant scenario.

If we do if ($required_derivative_scheme !== NULL && $required_derivative_scheme !== $derivative_scheme) { we still leave the vulnerability present if a site doesn't register a scheme with the route. This would fix the vulnerability for the most common offender (core). This has impacts on those extending the class as well as those creating/modifying routes.

We possibly could remove the '{scheme}' wildcard from the routes and only register the routes without wildcard and manually set the scheme value as part of the route definition. This still leaves the vulnerability present for code that uses the wildcard (contrib). This could break some read-only streamWrapper that depend upon core to have the existing wildcard route to provide support (the 'default' filesystem is used to store derivatives for read only streamWrappers.) I can think of no way a read-only streamWrapper would be able to reliability determine what route is the 'default' that they should add their scheme onto. This has impacts on any module modifying/creating routes.

We could add a new method "expectedDestinationScheme()" to avoid modifying the existing method, however I believe this may require a new class for each route to define the approved scheme (or some form of lookup that allows defining routes.) This is a scenario where a route name lookup could be trusted because its in control of the class. This has impacts on any module that extends the existing controller or attempts to implement a route based on the controller.

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.

catch’s picture

This has impacts on any module that extends the existing controller or attempts to implement a route based on the controller.

I'm not sure between the various options, but just in principle we don't need to provide backwards compatibility for controllers. https://www.drupal.org/about/core/policies/core-change-policies/bc-polic...

So even if that breaks a contrib or custom module, that'd be fine with a change record and release note.

The same with route definitions too to be honest.

We try not to go out of our way to break things like this, but if it's necessary there is nothing stopping us from doing it.

cmlara’s picture

Status: Needs work » Needs review
StatusFileSize
new6.89 KB

This probably should have been back to NR after #35

As noted by catch the goal is to not break, however if necessary breaking BC is permitted.

As noted in my previous posts, any BC layer here would leave a security vulnerability present and as such I contend meets the definition of "necessary" justifying no BC layer.

Rebased #21 on 11.x

patching file core/modules/image/image.routing.yml
patching file core/modules/image/src/Controller/ImageStyleDownloadController.php
Hunk #2 succeeded at 108 with fuzz 1.
Hunk #3 succeeded at 156 (offset 14 lines).
patching file core/modules/image/src/Routing/ImageStyleRoutes.php
patching file core/modules/image/tests/src/Functional/ImageStyleDownloadAccessControlTest.php

No interdiff as this is a rebase.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs change record updates

So taking a look I see there is now a new parameter that could be used in the service, but the CR doesn't have any code example of that and when to use. Think the CR could be tweaked some to be more clear.

Thanks

cmlara’s picture

I have added an example to the CR showing the required routing changes, and made it more clear the value is required.

cmlara’s picture

Status: Needs work » Needs review
smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

CR reads well. Lets see what the committers think about the need for BC or not.

needs-review-queue-bot’s picture

Status: Reviewed & tested by the community » Needs work

The Needs Review Queue Bot tested this issue.

While you are making the above changes, we recommend that you convert this patch to a merge request. Merge requests are preferred over patches. Be sure to hide the old patch files as well. (Converting an issue to a merge request without other contributions to the issue will not receive credit.)

smustgrave’s picture

Status: Needs work » Reviewed & tested by the community

Hiding files and converted #36 to an MR.

Please do not credit me if I did not other wise earn it as all I did was convert this.

Restoring status

bramdriesen’s picture

Issue summary: View changes

Fixed a typo and a few readability issues in the IS.

bramdriesen’s picture

Also verified all changes from the patch are in the MR. So the re-rtbc is double checked 🙂

quietone’s picture

I read the issue summary and comments. Thanks for the clear issue summary, it made reading the comments much easier. I did not find any unanswered questions.

@BramDriesen, thank you for confirming the change from patch to MR is correct.

The only think I can think of is that the release note snippet should mention the possible BC break.

  • longwave committed fd361543 on 11.x
    Issue #3298701 by cmlara, smustgrave, Rishabh Vishwakarma, Tanuj.,...
longwave’s picture

Status: Reviewed & tested by the community » Fixed

Committed fd36154 and pushed to 11.x. Thanks!

Not backportable due to 10.2.x due to the minor BC break. Updated the change record to more explicitly note this is a BC break for anyone using the controller as suggested by @quietone, and published it.

quietone’s picture

Publish the CR

Status: Fixed » Closed (fixed)

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

jurgenhaas’s picture

Unfortunately, this is a breaking change in 10.3.x, isn't it?

larowlan’s picture

Issue summary: View changes
Issue tags: +10.3.0 release notes
xjm’s picture

Priority: Normal » Major
Status: Closed (fixed) » Needs work

As noted in #3450244: Provide BC for ImageStyleDownloadController, we need to do a better job of explaining this in its release note (which should also link the CR).

smustgrave’s picture

Issue summary: View changes
Status: Needs work » Reviewed & tested by the community

Gave it a shot but not sure best spot to place in

needs-review-queue-bot’s picture

Status: Reviewed & tested by the community » Needs work
StatusFileSize
new90 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.

smustgrave’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: +no-needs-review-bot
xjm’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Fixed

Thanks @smustgrave!

Updated the IS with what we eventually settled on here. Thanks!

Status: Fixed » Closed (fixed)

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

sceefo’s picture

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

I have a situation where images are stored to s3 with flysystem_s3 but thumbnails are stored to local public files folder. After updating from 10.2.7 to 10.3.6 thumbnails start to fail due to this scheme check

if ($required_derivative_scheme !== $derivative_scheme) {
      throw new AccessDeniedHttpException("The scheme for this image doesn't match the scheme for the original image");
    }

Original image is s3 scheme and thumbnail is public. Any ideas how can I keep my thumbnails in public files while actual resources are in s3?

bramdriesen’s picture

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

Probably best to create a new ticket and reference this one instead of commenting on a closed/fixed issue.

xjm’s picture

Amending attribution.