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.
| Comment | File | Size | Author |
|---|---|---|---|
| #55 | 3298701-nr-bot.txt | 90 bytes | needs-review-queue-bot |
Issue fork drupal-3298701
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:
- 3298701-imagestyledownloadcontroller-routes-do
changes, plain diff MR !5340
Comments
Comment #2
cmlaraPatch originally presented by @effulgentsia, modified to latest 9.4.x branch.
Comment #3
cmlaraAdding 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.
Comment #4
cmlaraphpcs error cleanup.
Comment #7
driskell commentedJust 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?
Comment #8
cmlaraLooking 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)
Comment #9
smustgrave commentedBrought this up in slack in the #needs-review-queue-initiative channel and @cmlara talked about rescoping, #8
Comment #10
cmlaraDowngrading 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.
Comment #11
cmlaraRebased on 10.1.x
Removed the $public parameter per #8
Comment #12
cmlaraComment #14
smustgrave commented+1 from me but will this need a change record now? If not think it can be marked RTBC.
Comment #15
cmlaraCreated CR at https://www.drupal.org/node/3346038.
Comment #16
cmlaraOne more minor change, adding string parameter typehint to the new parameter and removing the NULL default.
Comment #17
smustgrave commentedThanks for the quick change record!
Comment #18
catchI 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.
Comment #19
rishabh vishwakarma commentedAddressed the suggestion from #18
Comment #20
smustgrave commentedThink we want a different type of error message.
Brought this up in slack #needs-review-queue-initative channel
From @catch
From @cmlara
Comment #21
tanuj. commentedChanging exception message as per suggestions from #20.
Comment #23
cmlaraRandom test failure, retest is green, back to NR.
Comment #24
smustgrave commentedThink 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.
Comment #26
larowlanCrediting @effulgentia
Comment #27
larowlanI've pinged security team folks in slack to ask some questions about this
Comment #29
larowlanShould 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
Comment #30
larowlanComment #31
cmlaraAs 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 '/'
Comment #32
benjifisher@cmlara:
The link in #31 is missing a '/' in 'aboutcore'. If I add that and scroll up the page, I see the paragraph,
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.
Comment #33
cmlaraNote: 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.
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.
Comment #35
catchI'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.
Comment #36
cmlaraThis 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
No interdiff as this is a rebase.
Comment #37
smustgrave commentedSo 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
Comment #38
cmlaraI have added an example to the CR showing the required routing changes, and made it more clear the value is required.
Comment #39
cmlaraComment #40
smustgrave commentedCR reads well. Lets see what the committers think about the need for BC or not.
Comment #41
needs-review-queue-bot commentedThe 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.)
Comment #43
smustgrave commentedHiding 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
Comment #44
bramdriesenFixed a typo and a few readability issues in the IS.
Comment #45
bramdriesenAlso verified all changes from the patch are in the MR. So the re-rtbc is double checked 🙂
Comment #46
quietone commentedI 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.
Comment #48
longwaveCommitted 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.
Comment #49
quietone commentedPublish the CR
Comment #51
jurgenhaasUnfortunately, this is a breaking change in 10.3.x, isn't it?
Comment #52
larowlanComment #53
xjmAs 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).
Comment #54
smustgrave commentedGave it a shot but not sure best spot to place in
Comment #55
needs-review-queue-bot commentedThe 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.
Comment #56
smustgrave commentedComment #57
xjmThanks @smustgrave!
Updated the IS with what we eventually settled on here. Thanks!
Comment #60
sceefo commentedI 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
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?
Comment #61
bramdriesenProbably best to create a new ticket and reference this one instead of commenting on a closed/fixed issue.
Comment #62
xjmAmending attribution.