Commit 5a42b47b authored by catch's avatar catch

Issue #2966327 by alexpott, mcdruid, dww, dsnopek, catch, pwolanin, larowlan,...

Issue #2966327 by alexpott, mcdruid, dww, dsnopek, catch, pwolanin, larowlan, tim.plunkett, Berdir, Sam152: Limit what can be called by a callback in render arrays to reduce the risk of RCE
parent 7da933ba
......@@ -16,7 +16,6 @@
use Drupal\Component\Utility\SortArray;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Element\Link;
use Drupal\Core\Render\HtmlResponseAttachmentsProcessor;
......@@ -777,96 +776,14 @@ function drupal_pre_render_link($element) {
/**
* Pre-render callback: Collects child links into a single array.
*
* This function can be added as a pre_render callback for a renderable array,
* usually one which will be themed by links.html.twig. It iterates through all
* unrendered children of the element, collects any #links properties it finds,
* merges them into the parent element's #links array, and prevents those
* children from being rendered separately.
*
* The purpose of this is to allow links to be logically grouped into related
* categories, so that each child group can be rendered as its own list of
* links if drupal_render() is called on it, but calling drupal_render() on the
* parent element will still produce a single list containing all the remaining
* links, regardless of what group they were in.
*
* A typical example comes from node links, which are stored in a renderable
* array similar to this:
* @code
* $build['links'] = array(
* '#theme' => 'links__node',
* '#pre_render' => array('drupal_pre_render_links'),
* 'comment' => array(
* '#theme' => 'links__node__comment',
* '#links' => array(
* // An array of links associated with node comments, suitable for
* // passing in to links.html.twig.
* ),
* ),
* 'statistics' => array(
* '#theme' => 'links__node__statistics',
* '#links' => array(
* // An array of links associated with node statistics, suitable for
* // passing in to links.html.twig.
* ),
* ),
* 'translation' => array(
* '#theme' => 'links__node__translation',
* '#links' => array(
* // An array of links associated with node translation, suitable for
* // passing in to links.html.twig.
* ),
* ),
* );
* @endcode
*
* In this example, the links are grouped by functionality, which can be
* helpful to themers who want to display certain kinds of links independently.
* For example, adding this code to node.html.twig will result in the comment
* links being rendered as a single list:
* @code
* {{ content.links.comment }}
* @endcode
*
* (where a node's content has been transformed into $content before handing
* control to the node.html.twig template).
*
* The pre_render function defined here allows the above flexibility, but also
* allows the following code to be used to render all remaining links into a
* single list, regardless of their group:
* @code
* {{ content.links }}
* @endcode
* @deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use
* \Drupal\Core\Render\Element\Link::preRenderLinks() instead.
*
* In the above example, this will result in the statistics and translation
* links being rendered together in a single list (but not the comment links,
* which were rendered previously on their own).
*
* Because of the way this function works, the individual properties of each
* group (for example, a group-specific #theme property such as
* 'links__node__comment' in the example above, or any other property such as
* #attributes or #pre_render that is attached to it) are only used when that
* group is rendered on its own. When the group is rendered together with other
* children, these child-specific properties are ignored, and only the overall
* properties of the parent are used.
* @see https://www.drupal.org/node/2966725
*/
function drupal_pre_render_links($element) {
$element += ['#links' => [], '#attached' => []];
foreach (Element::children($element) as $key) {
$child = &$element[$key];
// If the child has links which have not been printed yet and the user has
// access to it, merge its links in to the parent.
if (isset($child['#links']) && empty($child['#printed']) && Element::isVisibleElement($child)) {
$element['#links'] += $child['#links'];
// Mark the child as having been printed already (so that its links
// cannot be mistakenly rendered twice).
$child['#printed'] = TRUE;
}
// Merge attachments.
if (isset($child['#attached'])) {
$element['#attached'] = BubbleableMetadata::mergeAttachments($element['#attached'], $child['#attached']);
}
}
return $element;
@trigger_error('drupal_pre_render_links() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Render\Element\Link::preRenderLinks() instead. See https://www.drupal.org/node/2966725', E_USER_DEPRECATED);
return Link::preRenderLinks($element);
}
/**
......
......@@ -4,13 +4,14 @@
use Drupal\Component\Utility\Crypt;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface;
use Symfony\Component\Routing\Route;
/**
* Processes the outbound route to handle the CSRF token.
*/
class RouteProcessorCsrf implements OutboundRouteProcessorInterface {
class RouteProcessorCsrf implements OutboundRouteProcessorInterface, TrustedCallbackInterface {
/**
* The CSRF token generator.
......@@ -81,4 +82,11 @@ public function renderPlaceholderCsrfToken($path) {
];
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['renderPlaceholderCsrfToken'];
}
}
......@@ -7,13 +7,14 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Render\RendererInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a generic controller to render a single entity.
*/
class EntityViewController implements ContainerInjectionInterface {
class EntityViewController implements ContainerInjectionInterface, TrustedCallbackInterface {
use DeprecatedServicePropertyTrait;
/**
......@@ -110,6 +111,13 @@ public function view(EntityInterface $_entity, $view_mode = 'full') {
return $page;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['buildTitle'];
}
/**
* Provides a page to render a single entity revision.
*
......
......@@ -11,6 +11,7 @@
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Theme\Registry;
use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -20,7 +21,7 @@
*
* @ingroup entity_api
*/
class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterface, EntityViewBuilderInterface {
class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterface, EntityViewBuilderInterface, TrustedCallbackInterface {
use DeprecatedServicePropertyTrait;
/**
......@@ -142,6 +143,13 @@ public function view(EntityInterface $entity, $view_mode = 'full', $langcode = N
return $build;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['build', 'buildMultiple'];
}
/**
* {@inheritdoc}
*/
......
......@@ -1530,7 +1530,8 @@ function hook_entity_view_alter(array &$build, Drupal\Core\Entity\EntityInterfac
$build['an_additional_field']['#weight'] = -10;
// Add a #post_render callback to act on the rendered HTML of the entity.
$build['#post_render'][] = 'my_module_node_post_render';
// The object must implement \Drupal\Core\Security\TrustedCallbackInterface.
$build['#post_render'][] = '\Drupal\my_module\NodeCallback::postRender';
}
}
......
......@@ -15,6 +15,7 @@
use Drupal\Core\Form\Exception\BrokenPostRequestException;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\ElementInfoManagerInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\FileBag;
......@@ -26,7 +27,7 @@
*
* @ingroup form_api
*/
class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface {
class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface, TrustedCallbackInterface {
/**
* The module handler.
......@@ -1406,4 +1407,11 @@ protected function currentUser() {
return $this->currentUser;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['renderPlaceholderFormAction', 'renderFormTokenPlaceholder'];
}
}
......@@ -17,6 +17,9 @@
* Some render elements are specifically form input elements; see
* \Drupal\Core\Render\Element\FormElementInterface for more information.
*
* The public API of these objects must be designed with security in mind as
* render elements process raw user input.
*
* @see \Drupal\Core\Render\ElementInfoManager
* @see \Drupal\Core\Render\Annotation\RenderElement
* @see \Drupal\Core\Render\Element\RenderElement
......@@ -24,7 +27,7 @@
*
* @ingroup theme_render
*/
interface ElementInterface extends PluginInspectionInterface {
interface ElementInterface extends PluginInspectionInterface, RenderCallbackInterface {
/**
* Returns the element properties for this element.
......
......@@ -5,6 +5,7 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\Html as HtmlUtility;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element;
use Drupal\Core\Url as CoreUrl;
/**
......@@ -99,4 +100,105 @@ public static function preRenderLink($element) {
return $element;
}
/**
* Pre-render callback: Collects child links into a single array.
*
* This method can be added as a pre_render callback for a renderable array,
* usually one which will be themed by links.html.twig. It iterates through
* all unrendered children of the element, collects any #links properties it
* finds, merges them into the parent element's #links array, and prevents
* those children from being rendered separately.
*
* The purpose of this is to allow links to be logically grouped into related
* categories, so that each child group can be rendered as its own list of
* links if drupal_render() is called on it, but calling drupal_render() on
* the parent element will still produce a single list containing all the
* remaining links, regardless of what group they were in.
*
* A typical example comes from node links, which are stored in a renderable
* array similar to this:
* @code
* $build['links'] = array(
* '#theme' => 'links__node',
* '#pre_render' => array(Link::class, 'preRenderLinks'),
* 'comment' => array(
* '#theme' => 'links__node__comment',
* '#links' => array(
* // An array of links associated with node comments, suitable for
* // passing in to links.html.twig.
* ),
* ),
* 'statistics' => array(
* '#theme' => 'links__node__statistics',
* '#links' => array(
* // An array of links associated with node statistics, suitable for
* // passing in to links.html.twig.
* ),
* ),
* 'translation' => array(
* '#theme' => 'links__node__translation',
* '#links' => array(
* // An array of links associated with node translation, suitable for
* // passing in to links.html.twig.
* ),
* ),
* );
* @endcode
*
* In this example, the links are grouped by functionality, which can be
* helpful to themers who want to display certain kinds of links
* independently. For example, adding this code to node.html.twig will result
* in the comment links being rendered as a single list:
* @code
* {{ content.links.comment }}
* @endcode
*
* (where a node's content has been transformed into $content before handing
* control to the node.html.twig template).
*
* The preRenderLinks method defined here allows the above flexibility, but
* also allows the following code to be used to render all remaining links
* into a single list, regardless of their group:
* @code
* {{ content.links }}
* @endcode
*
* In the above example, this will result in the statistics and translation
* links being rendered together in a single list (but not the comment links,
* which were rendered previously on their own).
*
* Because of the way this method works, the individual properties of each
* group (for example, a group-specific #theme property such as
* 'links__node__comment' in the example above, or any other property such as
* #attributes or #pre_render that is attached to it) are only used when that
* group is rendered on its own. When the group is rendered together with
* other children, these child-specific properties are ignored, and only the
* overall properties of the parent are used.
*
* @param array $element
* Render array containing child links to group.
*
* @return array
* Render array containing child links grouped into a single array.
*/
public static function preRenderLinks($element) {
$element += ['#links' => [], '#attached' => []];
foreach (Element::children($element) as $key) {
$child = &$element[$key];
// If the child has links which have not been printed yet and the user has
// access to it, merge its links in to the parent.
if (isset($child['#links']) && empty($child['#printed']) && Element::isVisibleElement($child)) {
$element['#links'] += $child['#links'];
// Mark the child as having been printed already (so that its links
// cannot be mistakenly rendered twice).
$child['#printed'] = TRUE;
}
// Merge attachments.
if (isset($child['#attached'])) {
$element['#attached'] = BubbleableMetadata::mergeAttachments($element['#attached'], $child['#attached']);
}
}
return $element;
}
}
<?php
namespace Drupal\Core\Render\Element;
/**
* Indicates all public methods are safe to use in render callbacks.
*
* This should only be used when all public methods on the class are supposed to
* used as render callbacks or the class implements ElementInterface. If this is
* not the case then use TrustedCallbackInterface instead.
*
* @see \Drupal\Core\Render\Element\ElementInterface
* @see \Drupal\Core\Security\TrustedCallbackInterface
* @see \Drupal\Core\Render\Renderer::doCallback()
*/
interface RenderCallbackInterface {
}
......@@ -9,6 +9,9 @@
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Render\Element\RenderCallbackInterface;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Security\DoTrustedCallbackTrait;
use Drupal\Core\Theme\ThemeManagerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
......@@ -16,6 +19,7 @@
* Turns a render array into a HTML string.
*/
class Renderer implements RendererInterface {
use DoTrustedCallbackTrait;
/**
* The theme manager.
......@@ -210,10 +214,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
}
if (!isset($elements['#access']) && isset($elements['#access_callback'])) {
if (is_string($elements['#access_callback']) && strpos($elements['#access_callback'], '::') === FALSE) {
$elements['#access_callback'] = $this->controllerResolver->getControllerFromDefinition($elements['#access_callback']);
}
$elements['#access'] = call_user_func($elements['#access_callback'], $elements);
$elements['#access'] = $this->doCallback('#access_callback', $elements['#access_callback'], [$elements]);
}
// Early-return nothing if user does not have access.
......@@ -350,12 +351,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
}
// Build the element if it is still empty.
if (isset($elements['#lazy_builder'])) {
$callable = $elements['#lazy_builder'][0];
$args = $elements['#lazy_builder'][1];
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$new_elements = call_user_func_array($callable, $args);
$new_elements = $this->doCallback('#lazy_builder', $elements['#lazy_builder'][0], $elements['#lazy_builder'][1]);
// Retain the original cacheability metadata, plus cache keys.
CacheableMetadata::createFromRenderArray($elements)
->merge(CacheableMetadata::createFromRenderArray($new_elements))
......@@ -372,10 +368,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
// element is rendered into the final text.
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$elements = call_user_func($callable, $elements);
$elements = $this->doCallback('#pre_render', $callable, [$elements]);
}
}
......@@ -499,10 +492,7 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
// outputted text to be filtered.
if (isset($elements['#post_render'])) {
foreach ($elements['#post_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $this->controllerResolver->getControllerFromDefinition($callable);
}
$elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
$elements['#children'] = $this->doCallback('#post_render', $callable, [$elements['#children'], $elements]);
}
}
......@@ -756,4 +746,38 @@ protected function ensureMarkupIsSafe(array $elements) {
return $elements;
}
/**
* Performs a callback.
*
* @param string $callback_type
* The type of the callback. For example, '#post_render'.
* @param string|callable $callback
* The callback to perform.
* @param array $args
* The arguments to pass to the callback.
*
* @return mixed
* The callback's return value.
*
* @see \Drupal\Core\Security\TrustedCallbackInterface
*/
protected function doCallback($callback_type, $callback, array $args) {
if (is_string($callback)) {
$double_colon = strpos($callback, '::');
if ($double_colon === FALSE) {
$callback = $this->controllerResolver->getControllerFromDefinition($callback);
}
elseif ($double_colon > 0) {
$callback = explode('::', $callback, 2);
}
}
$message = sprintf('Render %s callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. Support for this callback implementation is deprecated in 8.8.0 and will be removed in Drupal 9.0.0. See https://www.drupal.org/node/2966725', $callback_type, '%s');
// Add \Drupal\Core\Render\Element\RenderCallbackInterface as an extra
// trusted interface so that:
// - All public methods on Render elements are considered trusted.
// - Helper classes that contain only callback methods can implement this
// instead of TrustedCallbackInterface.
return $this->doTrustedCallback($callback, $args, $message, TrustedCallbackInterface::TRIGGER_SILENCED_DEPRECATION, RenderCallbackInterface::class);
}
}
<?php
namespace Drupal\Core\Security;
/**
* Ensures that TrustedCallbackInterface can be enforced for callback methods.
*
* @see \Drupal\Core\Security\TrustedCallbackInterface
*/
trait DoTrustedCallbackTrait {
/**
* Performs a callback.
*
* If the callback is trusted the callback will occur. Trusted callbacks must
* be methods of a class that implements
* \Drupal\Core\Security\TrustedCallbackInterface or $extra_trusted_interface
* or be an anonymous function. If the callback is not trusted then whether or
* not the callback is called and what type of error is thrown depends on
* $error_type. To provide time for dependent code to use trusted callbacks
* use TrustedCallbackInterface::TRIGGER_SILENCED_DEPRECATION and then at a
* later date change this to TrustedCallbackInterface::THROW_EXCEPTION.
*
* @param callable $callback
* The callback to call. Note that callbacks which are objects and use the
* magic method __invoke() are not supported.
* @param array $args
* The arguments to pass the callback.
* @param $message
* The error message if the callback is not trusted. If the message contains
* "%s" it will be replaced in with the resolved callback.
* @param string $error_type
* (optional) The type of error to trigger. One of:
* - TrustedCallbackInterface::THROW_EXCEPTION
* - TrustedCallbackInterface::TRIGGER_DEPRECATION
* - TrustedCallbackInterface::TRIGGER_SILENCED_DEPRECATION
* Defaults to TrustedCallbackInterface::THROW_EXCEPTION.
* @param string $extra_trusted_interface
* (optional) An additional interface that if implemented by the callback
* object means any public methods on that object are trusted.
*
* @return mixed
* The callback's return value.
*
* @throws \Drupal\Core\Security\UntrustedCallbackException
* Exception thrown if the callback is not trusted and $error_type equals
* TrustedCallbackInterface::THROW_EXCEPTION.
*
* @see \Drupal\Core\Security\TrustedCallbackInterface
*/
public function doTrustedCallback(callable $callback, array $args, $message, $error_type = TrustedCallbackInterface::THROW_EXCEPTION, $extra_trusted_interface = NULL) {
$object_or_classname = $callback;
$safe_callback = FALSE;
if (is_array($callback)) {
list($object_or_classname, $method_name) = $callback;
}
elseif (is_string($callback) && strpos($callback, '::') !== FALSE) {
list($object_or_classname, $method_name) = explode('::', $callback, 2);
}
if (isset($method_name)) {
if ($extra_trusted_interface && is_subclass_of($object_or_classname, $extra_trusted_interface)) {
$safe_callback = TRUE;
}
elseif (is_subclass_of($object_or_classname, TrustedCallbackInterface::class)) {
if (is_object($object_or_classname)) {
$methods = $object_or_classname->trustedCallbacks();
}
else {
$methods = call_user_func($object_or_classname . '::trustedCallbacks');
}
$safe_callback = in_array($method_name, $methods, TRUE);
}
}
elseif ($callback instanceof \Closure) {
$safe_callback = TRUE;
}
if (!$safe_callback) {
$description = $object_or_classname;
if (is_object($description)) {
$description = get_class($description);
}
if (isset($method_name)) {
$description .= '::' . $method_name;
}
$message = sprintf($message, $description);
if ($error_type === TrustedCallbackInterface::TRIGGER_SILENCED_DEPRECATION) {
@trigger_error($message, E_USER_DEPRECATED);
}
elseif ($error_type === TrustedCallbackInterface::TRIGGER_WARNING) {
trigger_error($message, E_USER_WARNING);
}
else {
throw new UntrustedCallbackException($message);
}
}
return call_user_func_array($callback, $args);
}
}
<?php
namespace Drupal\Core\Security;
/**
* Interface to declare trusted callbacks.
*
* @see \Drupal\Core\Security\DoTrustedCallbackTrait
*/
interface TrustedCallbackInterface {
/**
* Untrusted callbacks throw exceptions.
*/
const THROW_EXCEPTION = 'exception';
/**
* Untrusted callbacks trigger E_USER_WARNING errors.
*/
const TRIGGER_WARNING = 'warning';
/**
* Untrusted callbacks trigger silenced E_USER_DEPRECATION errors.
*/
const TRIGGER_SILENCED_DEPRECATION = 'silenced_deprecation';
/**
* Lists the trusted callbacks provided by the implementing class.
*
* Trusted callbacks are public methods on the implementing class and can be
* invoked via
* \Drupal\Core\Security\DoTrustedCallbackTrait::doTrustedCallback().
*
* @return string[]
* List of method names implemented by the class that can be used as trusted
* callbacks.
*
* @see \Drupal\Core\Security\DoTrustedCallbackTrait::doTrustedCallback()
*/
public static function trustedCallbacks();
}
<?php
namespace Drupal\Core\Security;
/**
* Exception thrown if a callback is untrusted.
*/
class UntrustedCallbackException extends \RuntimeException {
}
......@@ -5,6 +5,7 @@
use Drupal\Component\Utility\NestedArray;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Security\TrustedCallbackInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Session\AccountInterface;
......@@ -15,7 +16,7 @@
/**
* Defines an object that holds information about a URL.
*/
class Url {
class Url implements TrustedCallbackInterface {
use DependencySerializationTrait;
/**
......@@ -887,4 +888,11 @@ public function setUnroutedUrlAssembler(UnroutedUrlAssemblerInterface $url_assem
return $this;
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['renderAccess'];
}
}
......@@ -3,8 +3,9 @@
namespace Drupal\big_pipe_regression_test;
use Drupal\big_pipe\Render\BigPipeMarkup;
use Drupal\Core\Security\TrustedCallbackInterface;
class BigPipeRegressionTestController {
class BigPipeRegressionTestController implements TrustedCallbackInterface {
const MARKER_2678662 = '<script>var hitsTheFloor = "</body>";</script>';
......@@ -43,4 +44,11 @@ public static function currentTime() {
];
}
/**
* {@inheritdoc}
*/
public static function trustedCallbacks() {
return ['currentTime'];
}
}