Originally reported by samuel.mortenson on February 20, 2017 to security.drupal.org #161890
Clear by the Drupal Security team for public resolution as a critical bug.
Problem/Motivation
Summary: Users with access to Twig files or inline Twig templates can abuse the Drupal's support for render arrays in Twig to execute arbitrary code.
===
Views has an arbitrary code execution vulnerability.
Replication steps:
- Enable Views
- As a user with the "Administer views" permission, create a new View of User Fields (any view with result rows will work)
- Add a "Custom text" field
- Enter the text
"{{ {"#lazy_builder": ["shell_exec", ["touch /tmp/hellofromviews"]]} }}" - Click "Update preview"
- See that the file "/tmp/hellofromviews" exists on your machine
This is possible as the "render_var" function is available to Twig entered through Views. As a result, users can enter arbitrary render arrays, which allows for code execution through callbacks like #lazy_builder, #process, #pre_render, #post_render, #access_callback, and potentially other keys. For this example, #lazy_builder was used as it had the most flexible callback method. It's also worth noting that "shell_exec" is used in the example as "eval" is not a function, and as a result can not be called as a part of call_user_func().
Proposed resolution
We have to basically prevent render arrays from being written directly in any template or assigned to a variable in twig and then rendered.
To accomplish this we wrap each render array in an object (extending ArrayObject that implements \Drupal\Core\Render\RenderableInterface) and override methods that would mutate the render array (like setting the value at an index).
This allows us to also make the logic in \Drupal\Core\Template\TwigExtension more consistent, since these variables are now objects that implement RenderableInterface and are treated like any other object.
=======
Range of suggested resolutions that were considered:
- Original suggestion by samuel.mortenson:
Instead of filtering dangerous render arrays entered this way in Views, I would suggest blocking render_array and \Drupal\Core\Template\TwigNodeVisitor outright, in a way that other modules could emulate if they too wanted to allow end-users to enter Twig through the UI.
The attached patch adds two new services, twig_strict and twig.strict_extension, which can be used to process Twig with a restricted set of components (functions, filters, etc.). As of right now the only known vulnerability is render_var, I have only restricted that function.
That service, in turn, can be enabled by users of the "inline_template" element, by passing the option "#strict_mode => TRUE". In the patch I had Views turn this on by default for all Views Plugins that use advanced rendering.
- A suggestion by pwolanin:
We are possibly doing this since render arrays are not object that can be cast to string.
It looks like we have to basically prevent render arrays from being written directly in any template or assigned to a variable in twig and then rendered.
Can Twig distinguish variables passed in verses created inside the template? We could possibly wrap each render array in an object (like ArrayObject that implements \Drupal\Core\Render\RenderableInterface) but might need to make it immutable inside Twig which could break existing templates or parts of the render flow.
- Another suggestion by pwolanin that samuel.mortenson proved was not sufficient:
We could recursively flag all render arrays going into twig as variables by setting a value for a # property that could not be set by someone creating render array in inline twig or even in a twig template with set.
From Samuel:
Here's an example of working around this idea in Twig, assuming "safe_render_array" is passed to Twig and already marked as safe.
{# Add malicious render array key to existing safe array #} {% set safe_render_array = safe_render_array|merge({'#lazy_builder': ["shell_exec", ["touch /tmp/hellofromviews"]}) %} {# Copy #_safe_to_render key from existing safe array to new array, to mock that it was already marked as safe #} {% set nasty_array = {"#lazy_builder": ["shell_exec", ["touch /tmp/hellofromviews"]], '#_safe_to_render': safe_render_array['#_safe_to_render']} %}A real world example may be more complex, but this should illustrate the problem with relying on an array key.
- A further suggestion
webform for 8.x (accidentally?) disables
"#lazy_builder"by adding extra properties to all render arrays. Depending on what the valid use case is for that, maybe we could do the same in Twig.
Remaining tasks
Get feedback from people using Twig on how elements of render arrays are accessed and used.
Decide on a secure approach
Implement the fix
Fix failing tests
Add more test coverage
User interface changes
none
API changes
May be some BC-breaking changes to how render arrays may be accessed or modified in templates.
Data model changes
none
| Comment | File | Size | Author |
|---|---|---|---|
| #79 | interdiff-2860607-73-78.txt | 4.54 KB | samuel.mortenson |
| #79 | 2860607-78.patch | 20.69 KB | samuel.mortenson |
| #36 | 2860607-36.patch | 13 KB | pwolanin |
| #33 | increment-2860607-32.txt | 10.37 KB | pwolanin |
| #33 | 2860607-32.patch | 12.45 KB | pwolanin |
Comments
Comment #2
pwolanin commentedComment #3
samuel.mortensonI'm a fan of @pwolanin's suggestion, "we have to basically prevent render arrays from being written directly in any template or assigned to a variable in twig and then rendered". If we're OK with preventing users from modifying render arrays passed to Twig, I think this is the most secure option.
My original suggestion of adding a new Twig extension that's explicitly "safe" is only desirable as it doesn't change the current behavior of how render arrays work with Twig, but isn't a generic or fool-proof solution.
Comment #4
lauriiiYikes! From a perspective of a front-end developer, the original proposal of creating twig_strict service sounds better. The way people theme Drupal includes very commonly modifying and creating render arrays in a template. There is a plan to introduce this convention for Drupal core here #2821399: [meta] Theme System Modernization Initiative.
Comment #5
samuel.mortensonGiven that feedback, here's the original twig_strict patch I submitted with the issue.
Comment #6
pwolanin commented@laurii - The suggested alternate render service is not sufficient in my mind since it's opt-in, so and contrib may neglect to use it, and it still leaves this hole open for people who can write Twig template files. I think the goal should be to make it as close as possible to safe for untrusted users to supply Twig templates.
Also - to be clear, we are only talking about preventing you from constructing an arbitrary array in Twig and having it treated as a valid render array by the render system.
Any Twig function or helper that takes params and returns a render array or renderable object should be able to work.
My general thinking at the moment is that we will need to wrap bare render arrays in an object implementing \ArrayAccess interface and \Drupal\Core\Render\RenderableInterface
We would build the class to not allow you to set values *but you could unset to support the |without filter), and if you get a value that is an array we'd also wrap that array in a new object before returning it.
By doing that we could change \Drupal\Core\Template\TwigExtension::renderVar so we only handle render arrays that come from an object implementing \Drupal\Core\Render\RenderableInterface
Comment #7
samuel.mortenson@pwolanin Supporting existing users that are already creating render arrays in Twig, and changing existing render array properties, is definitely tough. I agree that a generic solution is safer here, but if we remove this functionality we'll force users to implement preprocess functions if they want to create or modify a render array.
To support existing use cases and still use the wrapping object, I think the following would need to happen:
It's complicated, but unless the community can agree to break current Twig functionality, we need to have a solution that maintains safety while not breaking BC.
Comment #8
samuel.mortensonAnother funny option would be to not wrap the whole array, but before passing variables to Twig recursively wrap all unsafe property values in an object. Then, before rendering render arrays with the filter/function/traverser, you find all unsafe properties in the array, and if their values are objects you set them back to their original value, otherwise you unset them as they must have been set in Twig. It sounds complicated, but compared to wrapping the entire render array, might be simple to implement.
This allows us to have a generic solution as @pwolanin originally suggested, while only targeting the properties that we know are unsafe. In this way existing Twig users are only affected if, for some reason, they're setting callbacks in Twig (which I've never seen in the wild).
I'll work on a POC patch, but in the mean time let me know what you think.
Comment #9
pwolanin commented@samuel.mortenson - if you look at the issue laurii mentioned, it's about helpers that return render elements (arrays), not creating bare render arrays in Twig.
I really don't think that creating your own bare arrays to render can be a supported use?
re: wrapping unsafe elements - we don't necessarily know in advance what those are (or what contrib might add). Still it's worth a try to see if it works.
Comment #10
samuel.mortenson@pwolanin Ah, true. I didn't read through #2818121: Create alternative to Twig include function to improve Drupal integration so I wasn't aware that they were suggesting adding a new Twig tag with that kind of syntax. I think we need more community input to see how people are using render arrays in Twig, because it's possible that this is a feature not many people know about/have tried to use.
Comment #11
samuel.mortensonAssigning this to myself while I work on a POC and test coverage.
My idea from #8 and #7 regarding "safe" render properties would be really complex, and as @pwolanin points out doesn't cover contrib adding new unsafe properties. This is the list I came up with, but there's no way to tell if it's complete:
Given the complexity of maintaining a property blacklist, I'm working on wrapping all variables passed to Twig in objects, which is going OK so far.
Comment #12
samuel.mortensonSo I worked on the "It looks like we have to basically prevent render arrays from being written directly in any template or assigned to a variable in twig and then rendered." solution, and while the solution isn't' perfect, it does protect against the originally reported vulnerability.
Overview of the POC:
- A wrapping class called "ReadOnlyArrayObject" was created, which extends the standard \ArrayObject and implements no-op versions of any method that could change its contents.
- In Drupal's Twig implementation, I made it so that all context passed into Twig is wrapped in a "ReadOnlyArrayObject" object.
- In TwigExtension, I made ::renderVar only accept ReadOnlyArrayObject objects for rendering, throwing an error if an unwrapped array is passed for rendering.
Problems:
The solution has one major flaw, which is why I'm uploading the patch but not putting the issue into review. Since we're wrapping every render array in an object, "if" statements in Twig will always evaluate to true. This is because casting PHP objects to boolean (aka evaluating them as "truthy") will always be true, and that behavior cannot be overridden. This makes \ArrayObject behave differently than array(), as (bool) array() is false, and (bool) new \ArrayObject([]) is true.
The impact of this is that every Twig template that once has a statement like this:
would now need to write that as
If you apply the patch and install the standard profile, you should be able to see the problem on the homepage, as the "featured" region is visible but contains no blocks.
Summary:
The impact on frontend developers and existing themes/modules may be too big to accept an all-encompassing object wrapping solution. That said, this was an interesting exercise and might be useful for future solutions to this issue.
At this point, I'm not sure what the best mitigation for this problem is. Per #11, there are a lot of render array keys that could be considered "unsafe", and even if we wanted to filter them in Twig I don't know how we would.
If we want to re-use the POC work I did here, maybe we could combine it with the Twig strict filter and have modules like Views opt-into it?
Anyway, I hope we get something out of the code I wrote. Un-assigning for now.
Comment #13
pwolanin commented@samuel.mortenson thanks - sounds like a great start. I'll try to find time to look at extending that.
Comment #14
lauriii@samuel.mortenson thanks for the POC. It looks good but as you said, the change is quite disruptive. This would require multiple changes even in Drupal core.
I discussed earlier today with @pwolanin about the use case where render arrays are being created in Twig and we both agreed that it is not a good convention to create render arrays in templates. I misunderstood that this would prevent us from creating render array generators, but this never was the intention.
I also think that immutability for render arrays would make the whole subsystem more robust. Arrays just are not the easiest tool to use build something like that. I linked another issue which includes exploring around similar things but with slightly different approach.
Comment #15
samuel.mortensonDid a little more research on overriding the default PHP behavior of casting objects to boolean, and it appears that SimpleXML objects are the only exception to PHP's default behavior of always casting objects to true (ref http://php.net/manual/en/language.types.boolean.php#language.types.boole...). This RFC goes into some detail as well, and suggests allowing objets to overload their default cast behavior: https://wiki.php.net/rfc/object_cast_to_types.
In http://stackoverflow.com/a/14329778, a Stack Overflow user goes over the problem in good detail and gives an example of how you could extend SimpleXMLElement to have custom "truthy" behavior, which could be used in conjunction with the patch in #12 to get past the major flaw in the current POC.
I'll take a look at how crazy this is, and see if it can be used to improve the current patch.
Comment #16
samuel.mortensonI looked into using SimpleXMLElement instead of ArrayObject but it didn't pan out, I don't think we can get array-like behavior and array-like boolean casting with just the object.
Here's a really crazy solution to the problem I talk about in #12. Basically we use the TwigNodeVisitor to find every instance of a named variable in an if statement, and what that variable in a custom Twig function called "is_bool", which simply casts a variable to boolean unless that variable is a ReadOnlyArrayObject, in which case it determines truthiness based on "count($var) > 0".
Keep in mind that this is still POC code, but I'd like like to get some review to see if this direction makes sense, or if we need to step back and try another solution.
Comment #17
pwolanin commented@samuel.mortenson why is page.featured an empty array?
Comment #18
samuel.mortenson@pwolanin I'm guessing that by default, empty theme regions are represented by empty arrays.
Comment #19
catchComment #20
samuel.mortensonActually re-reading my patch from #16, we could not do any of the things in the interdiff and instead only wrap non-empty arrays. Should clear up a ton of the complexity, I'll work on a patch.
Comment #21
samuel.mortensonThat didn't take long. I regret working so long on #16 but am glad that we don't have to review that complexity. Let's kick off the testbot to see how much breaks.
Comment #23
samuel.mortensonThe warning that's causing multiple tests to fail is a result of us not settings proper flags for our ReadOnlyArrayObjects. Here's a new patch which fixes that (hopefully).
Comment #25
samuel.mortensonFound a bug in render_var where toRenderable() was called but not wrapped in a ReadOnlyArrayObject.
Comment #27
pwolanin commented@samuel.mortenson - I'm a little confused here. Why re-wrap render arrays instead of simple dealing with them inside the if?
Comment #28
samuel.mortenson@pwolanin This approach doesn't change the default behavior of
toRenderable, as the Twig engine is what specifically needs to make render arrays read-only. The interdiff from #25 prevents a notice from being thrown later on when$arg = $arg->getArrayCopy();is executed, although you're probably on the right track thinking that we could shuffle the logic in renderVar around to reduce the wrapping calls. I'll try to clean up the logic in my next patch.Comment #29
pwolanin commented@samuel.mortenson are you in IRC? Maybe we should talk, or let me know if you want me to try rolling a version of the patch.
Comment #30
samuel.mortenson@pwolanin I haven't started writing a new patch so feel free to work off of #25. It's in an OK place to be reviewed/refactored to be a bit cleaner, and is close to passing tests, which is good! We're further along than I expected. I'm not on IRC consistently but I'll try to be there later today.
Comment #31
pwolanin commentedThe docs on this option look like it may not be the right thing to use?
Comment #32
samuel.mortenson@pwolanin Ah, you're right. We would need to override
__set()in addition to using that flag. The reason I added that flag in #23 is that Twig is inconsistent with how it handles objects - sometimes it checks if an object implements \ArrayAccess and uses a call like$variable[$key], and other times it just checks if it's an object and runs$variable->$key. We could submit PRs to Twig to change its behavior to be more consistent, but I figured that would be better suited as a follow up.Comment #33
pwolanin commentedHere's a revised patch moving the class into the Drupal\Core\Render namespace so it can implement RenderableInterface
Did not try to address any test fails or add tests. It looks like the existing test could be a Kernel test instead of a web test?
Comment #34
pwolanin commented@samuel.mortenson - ok, I tried taking it out in #32 (and there were not flaming errors in the Drupal ui) before I saw your comment so let's see what the tests do.
Do you have specific file/line examples of where Twig tries to access properties?
Update:
testing in a php shell, overriding
function __set()does not seem to block the behavior.Probably we need to implement
__get()insteadComment #35
samuel.mortenson@pwolanin You can see the notice that was causing multiple tests to fail by applying the patch in #21 (before the flags), enabling the language module, and visiting /admin/config/regional/language/detection.
You should see this notice:
Your idea of just implementing
__getand not passing flags sounds good to me.Comment #36
pwolanin commentedHere's a patch adding that __get.
Comment #37
pwolanin commentedFrom discussion with @samuel.mortenson, fallback should be an exception since the variable could be something unexpected like a resource, not just an array.
Comment #41
samuel.mortensonTweaked template_preprocess_forum_list() and \Drupal\Tests\Core\Template\TwigExtensionTest a bit to support the ReadOnlyRenderArray object, which should fix most or all test failures. template_preprocess_forum_list() was setting render arrays as values in a Taxonomy Term object, which is totally non-standard. Instead of refactoring the entire function, I just wrapped that markup in a ReadOnlyRenderArray object. I've never seen this outside of forum, so I don't think it's going to be a common use case or one we should support.
Comment #42
pwolanin commentedGreat that you were able to knock out the last fails. Possibly for forum we should use this instead:
Though that's marked @internal
We should probably mark the ReadOnlyRenderArry class @internal also, since tit shouldn't be used outside the render system.
Comment #43
samuel.mortenson@pwolanin Agreed, adding the "Needs tests" tag as well as we should have unit tests that verify that ReadOnlyRenderArrays can't be modified in Twig.
Comment #44
lauriiiI think we should throw exceptions if something has been prevented from happening. This would improve the DX massively since it can only happen by a developer error and they would benefit if they could see that their code is not working as quickly as possible.
Comment #45
pwolanin commented@laurii why would this be disruptive to existing sites? Can you show me an example of a site's Twig template that would break?
I don't think that modifying render arrays in Twig was ever supposed to work, so if you were doing something that evil in Twig I don't think we have a responsibility to keep it working.
A filter or function can modify the render array by getting it as a plain array and then re-wrapping it to return, so I don't think that's a problem.
Comment #46
lauriiiFew of breaking examples:
Copied this directly from the Drupal Twig slack from a conversation that happened yesterday:
But also something like:
I agree that these examples are a bit evil, but they still would cause a break.
Comment #47
pwolanin commented@laurii - the second one should work fine with this patch (please double check and we should include a test) - it's just a copy and unset.
Perhaps the class naming needs to be be better since you can unset values so it's not truly read-only. Maybe something like "ProtectedRenderArray" ?
Comment #48
star-szrDiscussed with @xjm @alexpott @effulgentsia @catch and we agreed this is a critical security issue. And tentatively tagging with a security-ish tag (security people, please change to something more appropriate or remove if that makes more sense, thanks!).
Comment #49
star-szrSounds like just plain ol'
securitymight make sense, even though it's lowercase (gasp!).Comment #50
pwolanin commentedFrom discussion with laurii and Cottser in person, a way to provide an option for people who might be writing render arrays is to pull in the Twig theme function from http://cgit.drupalcode.org/components/tree/src/Template/TwigExtension.php.
Maybe that should go in 8.3 and this change in 8.4 so people have time to prepare?
Comment #51
pwolanin commentedQuick patch for name change to ProtectedRenderArray and rework of forum code.
NR for bot.
Comment #52
alexpottWhy not just $variables['forums'][$id]->description = ['#markup' => (string) $forum->description->value];
We don't want to see Markup::create() everywhere.
That said this change is tricky because custom / contrib could have done exactly the same thing as this. I think this fix needs to happen entirely within the render system and shouldn't impact other code.
Comment #53
alexpottThe stringification in #52 is not going to work. Is the problem being caused by the fact that $variables['forums'][$id] is an object?
Comment #54
samuel.mortensonYes, the problem is that the entity is having render arrays set on it, which is a bad practice that I've never seen anywhere else.
If the change feels awkward we can refactor the forum template and function more.
Edit: #41 has more detail on this.
Comment #55
pwolanin commentedRefactoring forum module seemed out of scope, so this was the most feasible fix
probably need to change the name of this?
Comment #57
star-szrIt sounds like maybe a good next step to just get this moving forward would be to refactor the forum.module theme layer code in a separate issue. Agreed with #41 that it doesn't seem like something we need to support. Created a separate issue to discuss that: #2901773: Refactor template_preprocess_forum_list() to not manipulate NodeInterface objects
I agree with #44 that this could benefit from profiling, adding that specific tag. During the D8 development cycle, all Twig variables were wrapped in a TwigReference object #2114563: Remove TwigReference with the help of 'without' filter replacing 'show'/'hide' functions.. Removing those objects didn't seem to cause a huge performance bump but I don't think the scenario that was tested was a super heavy one: #2114563-81: Remove TwigReference with the help of 'without' filter replacing 'show'/'hide' functions.
Another thing we need to be aware of is the TX/DX changes of having render arrays wrapped in terms of debugging and such. I suspect this will need a change record no matter what.
I also tried various tags, filters, etc. from core and Twig itself to try to further exploit this with the patch from #51 applied and wasn't able to come up with anything. Also, I confirmed that the iterable Twig test still works as expected with ProtectedRenderArray objects.
Comment #58
pwolanin commented@cottser - I think fixing this issue soon for 8.5.x and then fixing the forum code to allow us to remove the ugly method call would be better in terms of getting this done soon to let themers adjust.
Comment #59
star-szrIn general, I'm okay with a more temporary fix (with a @todo) here that allows us to solve the critical issue at hand as long as it's not disruptive.
Comment #60
pwolanin commentedOk, I will try to re-roll this with some @todo and adding the code from http://cgit.drupalcode.org/components/tree/src/Template/TwigExtension.php to core
Comment #61
samuel.mortensonHere's a straight re-roll on top of 8.5.x.
Comment #62
samuel.mortensonAdded unit test coverage, removing "Needs tests" flag! 😊
I'll be happy to write the change record after profiling is done - who normally does that for core?
Comment #63
samuel.mortensonCopy + paste got me again.
Comment #65
samuel.mortensonIn #62, I implemented a no-op for the
offsetUnsetmethod, which broke the Twig "without" filter. I've removed this as users expect to use this filter so we can't rightly break it. Tests should look better.Comment #66
samuel.mortensonCreated draft change record at https://www.drupal.org/node/2932417
Comment #68
elijah lynnReviewed this in person with samuel.mortenson at PNW Drupal Summit just now. He demonstrated the vulnerability to me by touching a file with a views edit. This could be a pretty significant chained vulnerability if there is an open admin permissions vulnerability. Some comments on names and comments below.
Why are we doing this? @todo comment on why
Why are we doing this? @todo comment on why
Would be nice to have a "... because otherwise Twig can allow any PHP callback to be excuted in certain situations."
s/objects/protected objects
@todo s/read only/protected
testProtectedTwigVariables()
s/ReadOnlyRenderArray/ProtectedRenderArray
s/twig_theme_test_read_only/twig_theme_test_protected
Comment #69
samuel.mortensonThis addresses all feedback from #68, and replaces all instances of "read only" with "protected".
@elijah-lynn brought up that this implementation wraps every Twig variable that is an array, even if it's not actually a render array. This means that if you had an array of strings called "classes", and passed that to Twig, then Twig tried to edit that array (add a class), this patch would break that functionality. I haven't seen this before, but it's worth bringing up. Should we have some fancy logic that tries to determine if a given array is a render array before wrapping it?
Comment #70
geek-merlinCode looks good. Small nit:
Leftover ReadOnly...
Comment #71
geek-merlin> This means that if you had an array of strings called "classes", and passed that to Twig, then Twig tried to edit that array (add a class), this patch would break that functionality. I haven't seen this before, but it's worth bringing up. Should we have some fancy logic that tries to determine if a given array is a render array before wrapping it?
I have seen such use cases in the wild. I'd say this opens an amazing can of works though:
* We can't tell from an array if it is a render array as an empty array is a valid render array as well as a valid non-render array.
* So the point is how this array is used. Finding out this in the general case is equivalent to the unsolvable halting problem.
If this is true, we can either
* make all arrays protected, which is a BC break for the themers
* let variable providers wrap renderables, determining their use, which is also a BC break
(OMG, how often do i hope my reasoning is wrong... ;-)
Comment #72
samuel.mortenson@axel.rutz Yeah this is a tough one. With the way the current patch works, if we used something like
preg_grep('/^#/', array_keys($possible_render_array)), we could see if any key of a given array starts with a hash, then we could assume that it's a render array and wrap it. This could have unintended consequences, but wouldn't be more insecure because we still prevent rendering non-protected arrays.Comment #73
samuel.mortensonThis addresses #70, thanks for the review.
Comment #74
geek-merlin#72: "contains a #foo key" is neither necessary (see empty array amongst others) nor sufficient (i had such nonrender arrays already) condition to know the array will be rendered.
so with that we'll have both problems:
* themers swearing their arrays are broken
* unrecognized render arrays passed in that
will be rendered after an attacker injected their evil codewill have exceptions thrownComment #75
samuel.mortenson@axel.rutz Are you saying that you've passed arrays to Twig with keys that included "#" which were _not_ render arrays? By restricting the scope of the wrapping we would not touch any other arrays passed to Twig.
Comment #76
geek-merlinYes i had arrays that (for non-twig reasons) had random-byte keys so some proportion of it has #foo keys. But that's imho the minor point.
Really problematic is the other point: Render arrays with no #foo keys. I don't see how to handle that.
Comment #77
samuel.mortenson@axel.rutz Could you provide an example of a render array that contains no keys that start with "#"? Just trying to get a handle on the logic we need to implement.
Comment #78
frobWhat about an array that has non-numeric keys (as in a dictionary) gets wrapped. If an array has only numeric keys (as in a list) then it doesn't get wrapped.
Comment #79
samuel.mortensonThis patch protects against what was brought up in #69, that arrays that are not render arrays passed to Twig may need to be edited in Twig. This protection is accomplished by relying on the
\Drupal\Core\Render\Elementclass to determine if a key in an array is recognized as a render array property (this just checks that the first character in the key is "#").This is-render-array check is recursive, which I didn't want to do, but I had to because the render system in Drupal allows deeply nested arrays that may not contain "#" keys until the very last layer. While rare, we should support these odd arrays. This is documented and tested in the interdiff.
A side-affect of this is that if you pass an array with a key that starts with a "#" to Twig, and that is not a render array, and you try to modify that array in Twig, that will no longer work. I think this is an incredibly rare case, so I'm not very worried about it. Hopefully tests pass. 🤞
Comment #81
samuel.mortensonThe error from #79 is happening because the Shortcut Block tries to render an array containing an empty array (i.e.
[[]]), which is not recognized as a render array and therefore not wrapped, which leads to an exception being thrown. If this was simply an empty array we could do anempty()check in TwigExtension.php, but this is a non-empty array. Not sure about the best way to approach this problem.Comment #82
geek-merlinValid real-life render arrays are amongst others: [], [[], []], [[[]]]
Comment #83
frob@axel.rutz
1. Do we have a test set for valid render arrays?
2. Also, does that continue on into endless sets of empty arrays?
3. Is any array that contains an empty array considered a valid render array?
Comment #84
samuel.mortensonSo I think we need Render API or other core maintainers to weigh in on this before proceeding.
To summarize the issue: We need to prevent render arrays from being created or edited in Twig. To accomplish this, all arrays passed to Twig as variables (context) are wrapped in an immutable object. This works fine, but it was brought up that some developers pass non-render-arrays to Twig that they may need to append to using something like the merge filter. An array of classes is an example of this.
To support the mutability of non-render-array arrays passed to Twig, conditional wrapping logic was added in #79. This works in most cases, but has an issue where in some cases a deeply-nested-empty array is passed to Twig and rendered using the
{{ variable }}syntax.Our options at this point:
This may require developers to change how they work with arrays in Twig that need to be mutable, but wrapping all arrays removes a lot of complexity and reduces the performance impact of this patch.
[[[]]]as a render array).I am in favor of option #1, as it removes complexity and is easier to test and document for users. There is going to be an impact for developers regardless of the solution we choose.
Comment #85
dawehnerInstead of preventing to edit arrays could we instead hash the #pre_render callbacks etc. and don't execute them unless the hash matches?
Comment #86
geek-merlin> Instead of preventing to edit arrays could we instead hash the #pre_render callbacks etc. and don't execute them unless the hash matches?
Interesting idea. This boils down to:
a) Change only array parts that look *dangerous* (i.e. contain callbacks)
b) Add a key that is effectively a code signature
While this does not solve the underlying problem (that we cannot surely know how an array will be used), a) greatly reduces the class of changed arrays, thus the chance of a falsely changed array, and b) may mitigate the effect of such a false change.
EDIT:
Just after sending i realized that you surely meant not to embed the hash in the render array, but to store it outside. Interesting.
So we just store a whitelist.
Then we have to work out: what to hash? only the callbacks? (Then we can as well store them.) Then we need to prove that a callback can not be abused by changing its arguments.
If we're not sure about that we must hash every array and all their sub-arrays (as they can be rendered separately).
Comment #87
pwolanin commentedI think reverting to #73 is the right approach for consistency and performance. A little breakage is better than seemingly unpredictable (or incorrect) behavior about what gets wrapped.
I thought we have e.g. an Attributes class to handle mutable attributes?
Comment #88
samuel.mortensonWe do - for the array of classes example an attributes object would be a great alternative.
Comment #89
geek-merlinOK so we say "Dear themers, all arrays are belong to us and will get wrapped. If you want mutable objects, use ArrayAccess." - right?
Some disruption, but rock solid!
Comment #90
markhalliwellUm. No.
[Base] themes like Drupal Bootstrap do heavy render array manipulation. On some real live working sites, this extends into the [Twig] templates themselves because it's often easier to manage than preprocessing. This includes creating things like icons or other various theme-related elements on the fly.
This mentality to "decrease themer's functionality even more" because "we must blanket bomb the entire system" is ludicrous.
This entire issue only exists due to it being yet another byproduct of the much larger issue: we still use antiquated render arrays for nearly everything; this isn't going away anytime soon.
The actual reason this issue exists is because we simply slapped some extension into Twig that renders variables using our Renderer service (which is far from perfect BTW).
From the OP, the primary concern is executing sensitive callbacks with something like the following:
{{ {"#lazy_builder": ["shell_exec", ["touch /tmp/hellofromviews"]]} }}The reality is: this has always been a reality in themes and can even be done in 7.x with far greater damage (PHPTemplate) lol
That being said, the introduction of Twig inline templates stored in the DB (for things like Views) has introduced a new little nitch that we hadn't yet considered, this being one of them (which we knew would likely happen, it was just a matter of time to find it).
From my perspective though, we'd be far better off introducing a validation of all render array callables (e.g. this really isn't about Twig at all) and filter out restricted/disabled functions in PHP.
Any "valid callback" should be a closure, service, Drupal namespaced method, or a procedural function that isn't blacklisted as mentioned above.
Just wrapping an already severely fragile render array can only spell major consequences (most likely for existing sites + contrib) yet unforeseen.
Comment #91
samuel.mortenson@markcarver @axel.rutz Just wanted to comment that any alternative solution that is less disruptive to themers is preferable (and welcome). This issue is just about mitigating the security vulnerability, and wrapping render arrays was just one approach. I don't want to give the impression that the current approach is what's necessarily going to land (we have no render api maintainer sign off as of yet).
I'll spend some time next week thinking about and maybe proof-of-concepting #85 and #90.
Comment #92
geek-merlin@markcarver, thank you for your viewpoint! You really sound as if you are quite angry though.
> [Base] themes like Drupal Bootstrap do heavy render array manipulation. On some real live working sites, this extends into the [Twig] templates themselves because it's often easier to manage than preprocessing. This includes creating things like icons or other various theme-related elements on the fly.
Interesting. Do you have example links for this?
Comment #93
jwilson3Example 1: the classic case is modifying the '#attributes' of a render array to add a class.
^ Source
Comment #94
diamondseaTo toss another idea into the mix, would it be safer to replace the calls to call_user_func() with a new drupal_call_user_func() and blacklist vulnerable/exploitable functions before executing, such as:
This would have the benefit of protecting other callback vectors that may be vulnerable.
(and the same protection for call_user_func_array() as drupal_call_user_func_array() )
Comment #95
samuel.mortenson@diamondsea I think that's the alternative we need to look into, to not disturb what themes are doing today with mutable arrays passed to Twig. Maintaining a blacklist isn't easy (we wan't to make the first iteration as complete as possible), but I think we can do it in a new issue.
Comment #96
geek-merlinGive me a blacklist and i'll find an exploit not on it...
(Think object method calls...)
Comment #97
diamondseaYeah, I noticed that with the SA-CORE-2018-001 exploit Dries linked to:
https://research.checkpoint.com/uncovering-drupalgeddon-2
though I'm not quite understanding how the code got uploaded in order to execute it somewhere the autoloader (?) could find the class to run it.
Comment #99
alexpottI think an alternative approach might be #2966327: Limit what can be called by a callback in render arrays to reduce the risk of RCE which sets out to limit what can be used as a callback in the render system.
Comment #100
geek-merlinStrong +1 for that!
Comment #101
alexpottI've re-rolled #2966327: Limit what can be called by a callback in render arrays to reduce the risk of RCE and it's ready for reviews.
Comment #102
alexpottDiscussed with @samuel.mortenson we agreed to postpone this on #2966327: Limit what can be called by a callback in render arrays to reduce the risk of RCE and bump that to critical.
Comment #103
gg4 commentedComment #105
geek-merlin#2966327: Limit what can be called by a callback in render arrays to reduce the risk of RCE was committed so it's time to wake this up.
Comment #106
samuel.mortensonThis issue may still need to be fixed to fix the Twig vulnerability: #2966711: Limit what can be called by a callback in form arrays, not sure if form callbacks can be triggered in Twig.
This will need to be committed to D9 in order to truly protect Twig: #3081025: Remove technical debt and complication from when doTrustedCallback() could either trigger errors or throw exceptions
Comment #107
chi commentedIf a user has access to Twig files he likely has access to php files as well. I would only concern about Twig templates that can be edited through admin interface (i.e. in Views UI). Besides code execution, access unprivileged users to editing Twig templates can lead to information disclosure which is not covered here. See #3083276: Split safe/unsafe functions/filters to their own (sub)modules for details.
What if we just create an alternative Twig service that does not support render arrays at all?
Comment #111
fathershawnHaving read through this issue and looked at #2966327: Limit what can be called by a callback in render arrays to reduce the risk of RCE what remains for this issue?
Comment #112
fathershawnComment #113
catchComment #116
stephencamilo commentedComment #117
maacl commentedComment #119
xjmComment #120
larowlanTagging for Bug Smash, hopefully we can get the issue summary update done
Comment #121
dwwTrying to make sense of this issue and understand if there's still any vulnerability here after #2966327: Limit what can be called by a callback in render arrays to reduce the risk of RCE went in.
Before I say anything else, huge hat tip to @samuel.mortenson for reporting this to the sec team, and for all the effort and thought that went into trying to plug this vulnerability! Also thanks to @pwolanin, @Cottser, the core committers, and others who have provided invaluable insights, examples, counter-examples, and so on.
I just manually tested the steps in the summary on a fresh 9.5.x site, and instead of a '/tmp/hellofromviews' file, I got this exception (as expected):
That much works as designed, and solves the originally reported problem. 🎉
In #116 @samuel.mortenson linked off to #3081025: Remove technical debt and complication from when doTrustedCallback() could either trigger errors or throw exceptions. At the time that comment was written, that issue was about changing the behavior of #2966327 to stop triggering deprecations and always throw exceptions. However, that security change went in as part of #3104307: Remove BC layers in various Drupal\Core components (seriously bad scope management there 😬). As of 9.0.0,
core/lib/Drupal/Core/Render/Renderer.phpalways invokesdoTrustedCallback()to throw exceptions for insecure render array callbacks. So we're now safe-by-default on that front, and #3081025 morphed into a new scope (which might never be committed, since we probably want to keep the flexibility for either deprecations or exceptions for other uses ofdoTrustedCallback()).That leaves only #2966711: Limit what can be called by a callback in form arrays. I'm not clear what's Twig-specific about that issue, or why this issue needs to stay open about the Twigy aspects of that (already marked critical) task. If you can trick Twig into calling form callbacks (yuck), it seems by far the best solution is to use a similar approach at #2966711 as we did for #2966327 and make form callbacks always secure, not just in some Twig-specific universe.
Therefore, I believe the right status here is "Closed (outdated)" or something, and we can focus any remaining energy on #2966711. However, I hate that using that status would not give credit to all the immense effort that went into this issue. 😢 "Transferring credit" seems like a PITA, noisy, and a waste of time. I'd rather just mark this "Closed (fixed)", instead. 😉
Meanwhile:
Comment #122
chi commentedUsers with access to Twig files are very likely have access to PHP files as well. That shouldn't be considered as a security issue. PHP code in templates has been used in Drupal 7 for years as well as in many other CMS powered by PHP template engine. I think we should only care about Twig templates that can be potentially edited via web UI, i.e. Views substitutions.
I propose we create an alternative Twig service (
twig.safe) without any support for render arrays. We could use it in Views substitution and inline templates. Contrib modules like Twig Input Filter could also benefit from it.Eventually, we could use that Twig environment for token replacements #2719477: Add support for Twig into Token::replace.
Comment #123
catchI think we probably need an extra follow-up for callbacks like #process for additional hardening that aren't using the trusted callback stuff either.
Comment #124
geek-merlin> I propose we create an alternative Twig service (twig.safe) without any support for render arrays. We could use it in Views substitution and inline templates. Contrib modules like Twig Input Filter could also benefit from it.
Big +1 on that.
Twig was announced as sandboxed, and i remember a talk saying "Hey, you even can give any non-trusted dudista access to your theming folder." I still deem it wrong to allow-by-default creating of render arrays (which has a big information disclosure potential) (instead of only explicitly allowing that), but it's like that now. Let's at least harden views and all the contrib modules using twig.
Comment #125
catchCould we get away with creating twig.unsafe, and then just switching over the theme layer to it? Would be a behaviour change for contrib, but would also fix any issues all at once.
Comment #126
geek-merlin> Could we get away with creating twig.unsafe, and then just switching over the theme layer to it? Would be a behaviour change for contrib, but would also fix any issues all at once.
+1 on that, the pattern "safe by default" is well proven and is friendly to the human factor.
Comment #127
larowlanI'm pretty certain that would break so many things
Comment #128
smustgrave commentedFollowing up on #121 I tested following the steps in the description. When I click update preview I get this in the logs which seems to be correct.
Twig\Error\RuntimeError: An exception has been thrown during the rendering of a template ("Render #lazy_builder callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was shell_exec. See https://www.drupal.org/node/2966725") in "__string_template__68d34f33f67b08a940b5e9cd800677fa" at line 1. in Twig\Template->displayWithErrorHandling() (line 419 of /var/www/html/vendor/twig/twig/src/Template.php).
So +1 for closed (outdated)
Comment #130
larowlanOther callbacks that don't use trusted callback interface but I think are safe:
* machine name exists, this isn't called during rendering, so won't get executed from twig, it only gets executed when validation fires which doesn't happen if you embed a form element in a twig file, it would need to be in something a form builder returned
* entity builder callbacks, same reason as above, plus you'd need an entity form for them to trigger
I think the next step here is to try #125 and see what breaks
Comment #131
smustgrave commentedMoving to NW for the IS update. Think a new title would useful too, but may just be me.
And for the implementation of #125