Problem/Motivation

Right now access checking happens because of the page flow not because of routing -- it's the kernel firing an event that causes access checking. Now, if you are doing something funny and route another request then you don't get access checking. This is a security weakness. Also an event subscriber can fire between the route matching and the access checker. This is a security weakness too.

This is a regression: in Drupal 7 you got a menu item by calling $item = menu_get_item($path) then you could just do $item && $item['access'] (arguably, that's not best and access denied should've changed $item itself into FALSE but that's past). Now, compare this to the current drupal_valid_path: you get a route from the route provider and then need to get a request, run a match and run an access check. This is extremely error prone and if a module author makes an error here that means she now has a sechole.

However as we learned in #2302563: Access check Url objects a route object in itself is not something that can be access checked; only together with parameters it can. That issue explores putting access checks of route derived objects this one explores putting access check on the route matching process which adds the route parameters to the request.

Proposed resolution

Remove the access subscriber. Add a simple class that wraps ChainRouter in all but the actual matching (matchRequest and match). Run the access check from matchRequest and make match call matchRequest . One can turn to the router.insecure service to get these done in a non-access-aware manner. Now the system defaults to secure and yet you can make a conscious decision to run insecure checks as Url::createFromPath indeed does.

Remaining tasks

Find out how else this can be exploited and then raise this to critical. I'm quite sure there is something really bad lurking here.

If this approach is accepted then file a followup to change the $access_check argument in match and createFromPath to default TRUE -- security should be opt out and not opt in (*cough*) but this patch is truly about just tightening up matchRequest, we can (and will) tighten match in the next step.

User interface changes

None.

API changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

chx’s picture

Issue summary: View changes
FileSize
7.69 KB

Ah yes, match needs to do this too.

chx’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 1: 2322809_1.patch, failed testing.

The last submitted patch, routighten.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
FileSize
7.72 KB

Odd. I tested this locally.

Status: Needs review » Needs work

The last submitted patch, 5: 2322809_2.patch, failed testing.

chx’s picture

Title: Tighten/broaded routing by access checking in situ » Tighten routing security by access checking in matchRequest
Issue summary: View changes
Status: Needs work » Needs review
FileSize
9.84 KB

Oh, doh! Shortcut is running Url::createFromPath() which calls \Drupal::service('router')->match('/' . $path, $access_check); which in turns throws a nice access denied cos we are anonymous at this point. So, I relaxed match a little. The point is tightening matchRequest. We can do more with this later -- and having at least an optional check on createFromPath is quite good -- it complements #2302563: Access check Url objects nicely.

chx’s picture

Issue summary: View changes

Status: Needs review » Needs work

The last submitted patch, 7: 2322809_7.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
FileSize
19.54 KB

Hopefully this fixes the test failures and also I wrote a test for the router class. Perhaps we should test the match method as well -- but for now, it's testing matchRequest, quite simple tests just takes a bit of setup.

Status: Needs review » Needs work

The last submitted patch, 10: 2322809_10.patch, failed testing.

chx’s picture

FileSize
19.58 KB

Quite odd but let's try to separate out the problems -- here's a match() version not doing any access checks.

chx’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 12: 2322809_12.patch, failed testing.

Crell’s picture

So... the idea here is to make it harder to do route matching without running access checks? That seems counter-productive. They are separate tasks in my mind. If someone wants to assemble these components together in such a way as to not have access checks at all in the system, they should be able to. Tying things together in order to make it less flexible in the name of protecting people from themselves doesn't have a great track record in Drupal. I would need to see something more than a "gut feeling" to reduce decoupling here.

Some code-related comments while I'm at it:

  1. +++ b/core/lib/Drupal/Core/Routing/Router.php
    @@ -0,0 +1,126 @@
    +class Router implements RouterInterface, RequestMatcherInterface {
    

    AccessAwareRouter?

  2. +++ b/core/lib/Drupal/Core/Routing/Router.php
    @@ -0,0 +1,126 @@
    +    $parameters = $this->chainRouter->matchRequest($request);
    +    // The controller is being handled by the HTTP kernel, so add an attribute
    +    // to tell us this is the controller request.
    +    $parameters['_controller_request'] = TRUE;
    +    $request->attributes->add($parameters);
    

    Even if this patch does move forward, the access check parts are still a separate task from the match itself. Move that to a separate method within the class.

  3. +++ b/core/lib/Drupal/Core/Routing/Router.php
    @@ -0,0 +1,126 @@
    +    try {
    +      if (!$this->accessManager->check($parameters[RouteObjectInterface::ROUTE_OBJECT], $request, $this->account)) {
    +        $e = new AccessDeniedHttpException();
    +      }
    +    }
    +    catch (\Exception $e) {
    +    }
    +
    +    $request->attributes->remove('_controller_request');
    +
    +    if ($e) {
    +      throw $e;
    +    }
    +    return $parameters;
    

    What we really want here is finally {}, but that wasn't added until PHP 5.5. :-( This is a really screwy way of emulating that, though. What's wrong with the approach in the code being replaced?

chx’s picture

FileSize
20.74 KB

Tying things together in order to make it less flexible in the name of protecting people from themselves does have a great track record in Drupal.

FTFY. That's what a secure system is: protects people from their stupidity at all costs. We make APIs that are easy to use securely and hard to use insecurely. Compare the quite stellar track record of core security to #2264041: Add a test to ensure title callbacks are not vulnerable to XSS (write up at drupal4hu.com/node/404 ).

Mind you, there is already a security weakness here as the IS mentions: weight yourself just below the access subscriber and be screwed.

Also, if you call matchRequest with a made up request then you won't get an access check at all. Actually core does that already...

Both of these are valid weaknesses.

Ongoing, this will make Url more secure too.

> AccessAwareRouter

renamed. The test too.

> Move that to a separate method within the class.

Moved.

> What's wrong with the approach in the code being replaced?

Code repetition.

chx’s picture

Status: Needs work » Needs review
chx’s picture

Note what this boils down to: what's the first driving principle of Drupal API design? Security. Everything else comes as a rather distant second, including performance, usability and all that jazz. This is what Drupal 8 moved from and it must move back.

Status: Needs review » Needs work

The last submitted patch, 16: 2322809_16.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
FileSize
22.05 KB

Should be down to three fails in ConfigTranslationUiTest . The fatal was in UrlTest::testCreateFromPath() as the mock route is using returnValueMap and that is strict about supplying all parameters including optional ones.

Status: Needs review » Needs work

The last submitted patch, 20: 2322809_20.patch, failed testing.

chx’s picture

Issue summary: View changes
Status: Needs work » Needs review
FileSize
24.8 KB

This has those three fails but I can't resist submitting this because -- PathValidator exactly does the "let's create a new request" dance and look how much simpler it became. You can just look at the diff of PathValidator.php to see the fundamental architectural difference here: $this->requestMatcher->matchRequest($request); does access checking by itself. Updated the issue summary too.

Status: Needs review » Needs work

The last submitted patch, 22: 2322809_22.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
FileSize
28.3 KB

Let's hope this passes tests.

realityloop’s picture

Correct me if I'm wrong..

My first thought when reading @crell's comment is that I'd expect that if a particular use case didn't need access checks it would be easy enough to write a custom module that returned true in all instances where it wasn't already handled by the permissions system.

Personally I'm in favor of having security as a priority especially where it may protect me from making a site insecure accidentally.

@crell I'd be really interested to hear and example of where this may be counter productive.

tim.plunkett’s picture

+++ b/core/core.services.yml
@@ -424,6 +424,9 @@ services:
   router:
+    class: Drupal\Core\Routing\AccessAwareRouter
+    arguments: ['@router.chain', '@access_manager', '@current_user']
+  router.unsafe:
     class: Symfony\Cmf\Component\Routing\ChainRouter
     calls:
       - [setContext, ['@router.request_context']]

This patch has changed since @Crell's comments. It now leaves the original router intact, but with a different service name.

I think this is a sufficient allowance for code that wants to run matching without applying access checks.

chx’s picture

Issue summary: View changes
FileSize
28.3 KB

> This patch has changed since @Crell's comments. It now leaves the original router intact, but with a different service name.

Erm, nope, we always made the service available, #24 had it as router.unsafe but safety and security is not the same so I renamed it to router.insecure and added this to the IS.

tim.plunkett’s picture

  1. +++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
    @@ -0,0 +1,128 @@
    +class AccessAwareRouter implements AccessAwareRouterInterface, RequestMatcherInterface {
    
    +++ b/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
    @@ -0,0 +1,31 @@
    +interface AccessAwareRouterInterface extends RouterInterface {
    

    I think this hierarchy is a little strange. Why not have AccessAwareRouterInterface extend RequestMatcherInterface as well?

  2. +++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
    @@ -0,0 +1,128 @@
    +  /**
    +   * @var ChainRouter
    +   */
    +  protected $chainRouter;
    +
    +  /**
    +   * @var AccessManagerInterface
    +   */
    +  protected $accessManager;
    +
    +  /**
    +   * @var AccountInterface
    +   */
    +  protected $account;
    

    These need full namespaces and one-liners

  3. +++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
    @@ -0,0 +1,128 @@
    +   * Constructs a router for drupal with access and upcasting.
    

    Drupal

  4. +++ b/core/lib/Drupal/Core/Routing/AccessAwareRouter.php
    @@ -0,0 +1,128 @@
    +   * @param \Symfony\Cmf\Component\Routing\ChainRouter $router
    +   *   The router.
    +   */
    +  public function __construct(ChainRouter $router, AccessManagerInterface $access_manager, AccountInterface $account) {
    

    Missing docs for the other params. And this should clarify why there is a second router.

  5. +++ b/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
    @@ -0,0 +1,31 @@
    +   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
    +   *   Thrown when access checking failed.
    +   */
    +  public function matchRequest(Request $request);
    ...
    +   * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
    +   *   Thrown when $access_check is enabled and access checking failed.
    +   */
    +  public function match($pathinfo);
    

    So now this interface is exactly the same as far methods and params.

    The only difference is the documented exceptions. Do you think a new interface is needed, or can they be on the implementing class?

  6. +++ b/core/lib/Drupal/Core/Url.php
    @@ -13,6 +13,7 @@
    +use Symfony\Component\Process\Exception\LogicException;
    

    Not needed

  7. +++ b/core/lib/Drupal/Core/Url.php
    @@ -123,7 +124,7 @@ public static function createFromPath($path) {
    -        $result = \Drupal::service('router')->match('/' . $path);
    +        $result = \Drupal::service('router.insecure')->match('/' . $path);
    

    This deserves a comment about why we purposefully use router.insecure

  8. +++ b/core/modules/config_translation/src/Controller/ConfigTranslationController.php
    @@ -167,15 +167,7 @@ public function itemPage(Request $request, $plugin_id) {
    -          $route_name = $route_request->attributes->get(RouteObjectInterface::ROUTE_NAME);
    
    @@ -228,35 +220,4 @@ public function itemPage(Request $request, $plugin_id) {
    -    catch (ParamNotConvertedException $e) {
    ...
    -    catch (ResourceNotFoundException $e) {
    

    You remove some classes here, but not their use statements.

  9. +++ b/core/modules/system/src/PathBasedBreadcrumbBuilder.php
    @@ -201,13 +202,12 @@ protected function getRequestForPath($path, array $exclude) {
    -      return NULL;
    ...
    -      return NULL;
    ...
    -      return NULL;
    

    Well, these were nice for clarity.

  10. +++ b/core/tests/Drupal/Tests/Core/Routing/AccessAwareRouterTest.php
    @@ -0,0 +1,86 @@
    +   * Tests the matchrRequest() function for access allowed.
    ...
    +   * Tests the matchrRequest() function for access denied.
    

    This says matchrR, with an extra "r"

Status: Needs review » Needs work

The last submitted patch, 27: 2322809_27.patch, failed testing.

The last submitted patch, 24: 2322809_24.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
FileSize
29.12 KB
10.07 KB

Addressed them.

Status: Needs review » Needs work

The last submitted patch, 31: 2322809_31.patch, failed testing.

chx’s picture

Status: Needs work » Needs review
FileSize
30.56 KB
1.97 KB
11.51 KB

Easy to fix, these fails are. I provided an extra interdiff to 27 so that it's easier to check that the review is addressed.

Status: Needs review » Needs work

The last submitted patch, 33: 2322809_33.patch, failed testing.

chx queued 33: 2322809_33.patch for re-testing.

tim.plunkett’s picture

Status: Needs work » Reviewed & tested by the community

I think this adequately addresses @Crell's concerns, and I like where we ended up. Thanks for working through this @chx!

chx’s picture

If someone is worried that Url is running route.insecure I filed a followup to check whether it can be removed, made optional , etc.

chx’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
31.44 KB
3.47 KB

Addressing dawehner's review from IRC

  1. Add back AccessAwareRouterInterface extending both interfaces as tim.plunkett asked
  2. Document the new AccessAwareRoute::checkAccess method.
  3. Added a todo AccessAwareRoute::checkAccess to refactor with finally once PHP 5.5 is available.
  4. Removed $this->container->get('router')->setCurrentUser($account); as unnecssary.
  5. Consequently removed the setCurrentUser from the class.
chx’s picture

FileSize
601 bytes
31.44 KB

One more small change.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
Issue tags: +WSCCI

Cool

+++ b/core/lib/Drupal/Core/Routing/AccessAwareRouterInterface.php
@@ -0,0 +1,32 @@
+
+interface AccessAwareRouterInterface extends RouterInterface, RequestMatcherInterface {

this. Comitters will see this anyway.

chx’s picture

FileSize
31.53 KB
614 bytes

OK. fixed that.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 41: 2322809_41.patch, failed testing.

Damien Tournoud queued 41: 2322809_41.patch for re-testing.

dawehner’s picture

Status: Needs work » Reviewed & tested by the community

No it didn't, yes it did, not it didn't!

catch’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/core.services.yml
    @@ -418,12 +418,15 @@ services:
    +  router.insecure:
    

    Pretty sure it should be unsecure, not insecure.

    Also it isn't unsecure if you use it properly, could do with a more descriptive name.

  2. +++ b/core/lib/Drupal/Core/Url.php
    @@ -123,7 +123,9 @@ public static function createFromPath($path) {
    +        $result = \Drupal::service('router.insecure')->match('/' . $path);
    

    While it's true that Url objects might be created and stored for different users, this is also the most common place where you might or might not want access checks. Are you supposed to check access to the route before using this? I don't see any docs in the patch on how to do that.

chx’s picture

Status: Needs work » Needs review

http://english.stackexchange.com/a/19655

Unsecure is not a word as far as I can tell.

http://en.wikipedia.org/wiki/Computer_insecurity
and so on.

For Url objects, there is #2323721: [sechole] Link field item and menu link information leakage and #2302563: Access check Url objects the latter adding the access call on the Url object itself that's why there's nothing here. This is being solved , right now just not in this issue.

chx’s picture

FileSize
1.02 KB
32.32 KB

Documented how the Url factory methods work regarding security.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Thanks for clarifying this!

chx’s picture

FileSize
32.35 KB
2.78 KB

Renamed to router.no_access_checks.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 49: 2322809_49.patch, failed testing.

chx’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
32.35 KB

Typo in core.services.yml

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 51: 2322809_51.patch, failed testing.

The last submitted patch, 51: 2322809_51.patch, failed testing.

chx’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
32.36 KB

Sigh. This is so hard.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 54: 2322809_53.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Reviewed & tested by the community
FileSize
32.4 KB
1.44 KB

Trivial fix.

dawehner’s picture

While reading this I realized that we potentially introduced a non-trivial regression.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 57: router-access-2322809-57.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
33.23 KB
1.96 KB

That expectation shouldn't be in setUp, but in the methods that need it.

dawehner’s picture

FileSize
33.53 KB
3.28 KB

Sometimes phpunit is quite lovely!

tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

Ah, yours is better.

dawehner’s picture

Oh I didn't actually wanted to do a issue-queue push --force but well, this happened :(

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Committed 450cc98 and pushed to 8.0.x. Thanks!

diff --git a/core/lib/Drupal/Core/Path/PathValidator.php b/core/lib/Drupal/Core/Path/PathValidator.php
index 0c1580b..d363417 100644
--- a/core/lib/Drupal/Core/Path/PathValidator.php
+++ b/core/lib/Drupal/Core/Path/PathValidator.php
@@ -50,10 +50,6 @@ class PathValidator implements PathValidatorInterface {
    *   The route provider.
    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
    *   The request stack.
-   * @param \Drupal\Core\Access\AccessManagerInterface $access_manager
-   *   The access manager.
-   * @param \Drupal\Core\Session\AccountInterface $account
-   *   The user account.
    */
   public function __construct(RequestMatcherInterface $request_matcher, RouteProviderInterface $route_provider, RequestStack $request_stack) {
     $this->requestMatcher = $request_matcher;
@@ -76,7 +72,7 @@ public function isValid($path) {
       return FALSE;
     }
 
-    // We can not use $this->requestMatcher->match() beccause we need to set
+    // We can not use $this->requestMatcher->match() because we need to set
     // the _menu_admin attribute to indicate a menu administrator is running
     // the menu access check.
     $request = RequestHelper::duplicate($this->requestStack->getCurrentRequest(), '/' . $path);

Fixed on commit.

  • alexpott committed 450cc98 on 8.0.x
    Issue #2322809 by chx, dawehner, tim.plunkett: Tighten routing security...
chx’s picture

Re #15

I would need to see something more than a "gut feeling" to reduce decoupling here.

How about a security hole? #2323721: [sechole] Link field item and menu link information leakage

Status: Fixed » Closed (fixed)

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

Fabianx’s picture

Title: Tighten routing security by access checking in matchRequest » Do not depend on event subscribers for security: Tighten routing security by access checking in matchRequest
Parent issue: » #2357719: [meta] Audit the event subscriber system for security

Tagging a parent issue and adding a title related to the new security initiative