I filed this issue has a security issue first and got the ok to file it as a public issue since the configure any layout permission is a restrict access permission. @see https://www.drupal.org/drupal-security-team/security-advisory-process-an...

Problem/Motivation

Steps to reproduce
1. Install the Layout Builder module and enable defaults and overrides for the article content type
2. Create a unpublished article node
2. Login as user with "configure any layout" permission
3. View node view page node/1
4. Get "access denied"
5. Go to node/1/layout

Expected result:
403
Actual result:
The Layout Builder representation of the node, with all field values displayed

Proposed resolution

User must have View access to an entity to create/update a layout override.

Remaining tasks

N/A

User interface changes

N/A

API changes

N/A

Data model changes

N/A

Release notes snippet

Previously, users who didn't have access to view individual entities were granted access to configure the layout for that entity (if per-entity layout configuration was enabled). This implicit access has been removed. Site owners should ensure that all content editor roles have access to view the content that they are configuring the layout for.

CommentFileSizeAuthor
#48 3028490-access-48-PASS.patch9.48 KBtim.plunkett
#48 3028490-access-48-FAIL.patch1.83 KBtim.plunkett
#41 3028490-access-fix-41-interdiff.txt1.18 KBtim.plunkett
#41 3028490-access-fix-41-D86.patch9.61 KBtim.plunkett
#41 3028490-access-fix-41-D87.patch8.86 KBtim.plunkett
#36 3028490-access-fix-36-interdiff.txt2.33 KBtim.plunkett
#36 3028490-access-fix-36-D87.patch8.62 KBtim.plunkett
#36 3028490-access-fix-36-D86.patch9.4 KBtim.plunkett
#30 3028490-access-fix-30-D86.patch8.57 KBtim.plunkett
#30 3028490-access-fix-30-D87.patch7.8 KBtim.plunkett
#27 3028490-access-fix-27-interdiff.txt2.59 KBtim.plunkett
#27 3028490-access-fix-27.patch7.8 KBtim.plunkett
#24 3028490-access-fix-24-D87-interdiff.txt2.36 KBtim.plunkett
#24 3028490-access-fix-24-D87.patch7.3 KBtim.plunkett
#24 3028490-access-fix-24-D86.patch8.07 KBtim.plunkett
#21 3028490-access-fix-21-interdiff.txt577 bytestim.plunkett
#21 3028490-access-fix-21-D86.patch8.07 KBtim.plunkett
#21 3028490-access-fix-21-D87.patch8.06 KBtim.plunkett
#20 Screen Shot 2019-02-07 at 7.42.13 PM.png149.21 KBkristen pol
#20 Screen Shot 2019-02-07 at 7.42.00 PM.png130.54 KBkristen pol
#20 Screen Shot 2019-02-07 at 7.42.51 PM.png304.18 KBkristen pol
#20 Screen Shot 2019-02-07 at 7.42.38 PM.png218.55 KBkristen pol
#20 Screen Shot 2019-02-07 at 7.40.02 PM.png141.35 KBkristen pol
#13 3028490-access-fix-12-interdiff.txt14.68 KBtim.plunkett
#13 3028490-access-fix-12.patch8.01 KBtim.plunkett
#2 3028490-access-2-PASS.patch12.19 KBtim.plunkett
#2 3028490-access-2-FAIL.patch1.59 KBtim.plunkett

Comments

tedbow created an issue. See original summary.

tim.plunkett’s picture

StatusFileSize
new1.59 KB
new12.19 KB
tedbow’s picture

Status: Active » Needs review

Running tests

The last submitted patch, 2: 3028490-access-2-FAIL.patch, failed testing. View results

r.aubin’s picture

Tested and the passing patch worked for me. Note that the steps to reproduce did not include enabling Layout builder for your type of content as well as per-content-item layouts. These 2 checkboxes are found on the "Manage Display" tab of your content type configuration.

/admin/structure/types/manage/content_type_name/display

r.aubin’s picture

Status: Needs review » Reviewed & tested by the community
tim.plunkett’s picture

Issue summary: View changes

Ah yes, these STR predate that opt-in checkbox.

xjm’s picture

Just documenting here that @tedbow and @tim.plunkett worked on the patch for this issue in the original private issue, so it has had peer code review there as well. Updating issue credit.

xjm’s picture

Status: Reviewed & tested by the community » Needs work
Issue tags: +Needs change record
+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -253,20 +241,6 @@ public function buildLocalTasks($base_plugin_definition) {
-  /**
-   * Determines if this entity type's ID is stored as an integer.
-   *
-   * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
-   *   An entity type.
-   *
-   * @return bool
-   *   TRUE if this entity type's ID key is always an integer, FALSE otherwise.
-   */
-  protected function hasIntegerId(EntityTypeInterface $entity_type) {
-    $field_storage_definitions = $this->entityFieldManager->getFieldStorageDefinitions($entity_type->id());
-    return $field_storage_definitions[$entity_type->getKey('id')]->getType() === 'integer';

At a minimum, the removal of this method should have a CR. Ideally, it would be deprecated instead of removed. https://www.drupal.org/core/deprecation#internal

When we are considering making internal BC breaks, the question shouldn't be "Is there any reason not to get rid of this?" There are two cases where we should break BC instead of deprecating:

  1. The BC break is required to solve the issue (e.g., when a form controller needs to be rewritten for a UI change, or for the delegation issue).
  2. Leaving the undesired code around is actually dangerous.

I don't think this method meets either of those criteria. It seems fairly harmless to me, so I'd propose just deprecating it and the EFM service injection. Another advantage of that is that the patch gets a lot smaller.

xjm’s picture

Priority: Normal » Major

While mulling #9, I thought of something that actually sort of trumps the discussion: This is a security issue. It's a public one because it's an administrative access bypass, but still a security issue. For that reason, we should backport this to 8.6.x; otherwise, 8.6.x will have a publicly disclosed administrative access bypass until its EOL in December. And in order to backport it to a patch release, we should make the patch as non-disruptive as possible (meaning avoiding even internal breaks like constructor service fixes).

xjm’s picture

Issue tags: +8.6.8 release notes, +Needs release note

Also needs a release note snippet (basically a sentence linking the CR).

xjm’s picture

Version: 8.7.x-dev » 8.6.x-dev

The current patch does not apply to 8.6.x, but maybe a minimal-er version will. Otherwise we can create the 8.6.x patch after we've finalized the 8.7.x one.

tim.plunkett’s picture

This is the smallest possible change.
Still needs CR.

Should apply to both branches

tim.plunkett’s picture

Status: Needs work » Needs review

Status: Needs review » Needs work

The last submitted patch, 13: 3028490-access-fix-12.patch, failed testing. View results

tim.plunkett’s picture

Status: Needs work » Needs review
xjm’s picture

Status: Needs review » Needs work

8.6.x is failing though:

1) Drupal\Tests\layout_builder\Unit\OverridesSectionStorageTest::testBuildRoutes
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
     'entity.from_canonical.canonical' => Symfony\Component\Routing\Route Object (...)
     'layout_builder.overrides.from_canonical.view' => Symfony\Component\Routing\Route Object (...)
     'layout_builder.overrides.from_canonical.save' => Symfony\Component\Routing\Route Object (...)
-    'layout_builder.overrides.from_canonical.discard_changes' => Symfony\Component\Routing\Route Object (...)
@@ @@
+    'layout_builder.overrides.from_canonical.cancel' => Symfony\Component\Routing\Route Object (...)

/var/www/html/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php:488

FAILURES!
Tests: 8, Assertions: 11, Failures: 1.
xjm’s picture

Status: Needs work » Needs review

Oh, I see this is what is mentioned in #17. So NR for he 8.7.x patch and then a backport.

xjm’s picture

Since this would be a critical issue for a stable module, marking as a stable blocker.

kristen pol’s picture

Thanks for the patch. One thing I noticed that maybe should be added?

+++ b/core/modules/layout_builder/tests/src/Functional/LayoutBuilderTest.php
@@ -283,6 +283,37 @@ public function testLayoutBuilderUi() {
+    $assert_session->pageTextNotContains('The first node body');
+    $assert_session->pageTextContains('Access denied');
+
+    $this->drupalGet('node/1/layout');
+    $assert_session->pageTextNotContains('The first node body');

Add another 'Access denied' assertion for layout page?

==

I tested this and it is showing access denied for both node/1 and node/1/layout now as expected.

Marking back to "Needs work" in case the test should be updated.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new8.06 KB
new8.07 KB
new577 bytes

Good point @Kristen Pol, idk why I didn't add that second check in the first place.

Here's an update 8.7 and 8.6 patch

The last submitted patch, 21: 3028490-access-fix-21-D87.patch, failed testing. View results

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

Looks good!

tim.plunkett’s picture

Issue summary: View changes
Status: Reviewed & tested by the community » Needs review
Issue tags: -Needs change record, -8.6.8 release notes, -Needs release note +8.6.10 release notes, +Blocks-Layouts
StatusFileSize
new8.07 KB
new7.3 KB
new2.36 KB

Added a CR https://www.drupal.org/node/3032823 and wrote a release notes snipppet

The 8.7 patch needed a reroll, the 8.6 patch did not but reuploading anyway just to be safe.

tedbow’s picture

The CR looks good. and the rerolls looks good.

RTBC! 🤞

tedbow’s picture

Status: Needs review » Reviewed & tested by the community
tim.plunkett’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new7.8 KB
new2.59 KB

Discussed with @xjm and @tedbow, specifically about why we need the else statement.

Turns out, we don't.
$entity_type->hasLinkTemplate('canonical') is checked before entering into this loop.
And the canonical link is used to build the path for this route below.

phenaproxima’s picture

  1. +++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
    @@ -209,10 +209,8 @@ public function buildRoutes(RouteCollection $collection) {
    -      $requirements = [];
    -      if ($this->hasIntegerId($entity_type)) {
    -        $requirements[$entity_type_id] = '\d+';
    -      }
    +      // Retrieve the requirements from the canonical route.
    +      $requirements = $collection->get("entity.$entity_type_id.canonical")->getRequirements();
    

    Nice change. This seems a lot less brittle!

  2. +++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php
    @@ -294,6 +294,14 @@ public function testBuildRoutes() {
    +    $from_canonical = $this->prophesize(EntityTypeInterface::class);
    +    $from_canonical->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
    +    $from_canonical->hasViewBuilderClass()->willReturn(TRUE);
    +    $from_canonical->hasLinkTemplate('canonical')->willReturn(TRUE);
    +    $from_canonical->getLinkTemplate('canonical')->willReturn('/entity/{entity}');
    +    $from_canonical->hasHandlerClass('form', 'layout_builder')->willReturn(TRUE);
    

    Nit: I think it's possible to just do new ContentEntityType([...]), and not mock anything here. Might be a bit easier to read...but this will work too. That's why it's a nitpick. :)

Status: Needs review » Needs work

The last submitted patch, 27: 3028490-access-fix-27.patch, failed testing. View results

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new7.8 KB
new8.57 KB

Ughh posted the patch for the wrong version.

#28.2 Yes I considered that, but I decided mocking the specific methods instead of messing with annotation-based array structure was cleaner.

Status: Needs review » Needs work

The last submitted patch, 30: 3028490-access-fix-30-D86.patch, failed testing. View results

tedbow’s picture

Status: Needs work » Reviewed & tested by the community

The test fail in the previous comment is because of Drupalci weirdness. Submitted retest.
This looks good. RTBC 🎉(drupalci will push back if another test fail.)

I manually tested this with node and user profiles.

Users who don't have access the canonical route/view access can't access the /layout page.

The last submitted patch, 30: 3028490-access-fix-30-D87.patch, failed testing. View results

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 30: 3028490-access-fix-30-D86.patch, failed testing. View results

xjm’s picture

OK, well, those look like real fails... apparently we do need those if checks for uninstallation an may need the fallback for whatever entities have it as NULL. :) And an inline comment explaining about those entities that don't have routes.

tim.plunkett’s picture

Status: Needs work » Needs review
StatusFileSize
new9.4 KB
new8.62 KB
new2.33 KB

I was slightly wrong in #27. Turns out that saying you have a canonical route and having a canonical route are different things.

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

Looks good!

xjm’s picture

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -230,6 +230,10 @@ public function buildRoutes(RouteCollection $collection) {
+      // Ensure the canonical route exists.
+      if (!$collection->get("entity.$entity_type_id.canonical")) {
+        continue;
+      }

So what are the cases where it doesn't exist, and what are the consequences of this for those cases? Is access granted or denied?

xjm’s picture

Status: Reviewed & tested by the community » Needs review
tim.plunkett’s picture

Status: Needs review » Reviewed & tested by the community

In order to have a Layout Builder UI, we require that the entity type specify a canonical route.
When we check that, we don't have the ability to ensure that they actually provided one until we're in this loop.

So, this is the same behavior as if they never claimed to provide one: there will not be a Layout Builder UI provided.

Meaning they will get a 404.

This is reflected in the test coverage I added in the interdiff.

tim.plunkett’s picture

Status: Reviewed & tested by the community » Needs review
StatusFileSize
new8.86 KB
new9.61 KB
new1.18 KB

The interdiff in #36 illustrates how unhelpful and brittle the current tests are for Layout Builder routing.
I opened #3033439: Refactor OverridesSectionStorageTest::testBuildRoutes() and DefaultsSectionStorageTest::testBuildRoutes() to be functional tests to fix that.

Additionally, the placement of that continue statement was misleading. I moved the check to the top of the loop and gave it a better comment.

tedbow’s picture

Status: Needs review » Reviewed & tested by the community

This looks good. The comment is much clearer to show that the entity type is totally skipped if it does not provide a canonical route.

xjm’s picture

So @tim.plunkett assures me that this test code:

+++ b/core/modules/layout_builder/tests/src/Unit/OverridesSectionStorageTest.php
@@ -294,6 +294,15 @@ public function testBuildRoutes() {
+    $canonical_link_no_route = $this->prophesize(EntityTypeInterface::class);
+    $canonical_link_no_route->entityClassImplements(FieldableEntityInterface::class)->willReturn(TRUE);
+    $canonical_link_no_route->hasViewBuilderClass()->willReturn(TRUE);
+    $canonical_link_no_route->hasLinkTemplate('canonical')->willReturn(TRUE);
+    $canonical_link_no_route->getLinkTemplate('canonical')->willReturn('/entity/{entity}');
+    $canonical_link_no_route->hasHandlerClass('form', 'layout_builder')->willReturn(TRUE);
+    $entity_types['canonical_link_no_route'] = $canonical_link_no_route->reveal();
+    $this->entityFieldManager->getFieldStorageDefinitions('canonical_link_no_route')->shouldNotBeCalled();

...somehow adds test coverage that ensures that the user gets a 404 when the conditions for the continue are met. #3033439: Refactor OverridesSectionStorageTest::testBuildRoutes() and DefaultsSectionStorageTest::testBuildRoutes() to be functional tests will definitely help make this clearer.

+++ b/core/modules/layout_builder/src/Plugin/SectionStorage/OverridesSectionStorage.php
@@ -227,13 +227,15 @@ private function extractEntityFromRoute($value, array $defaults) {
+      // If the canonical route does not exist, do not provide any Layout
+      // Builder UI routes for this entity type.

+1 for this comment; it is much clearer and with the new code location, makes it clear we're not adding a different kind of access bypass for entities that don't have a canonical route.

xjm’s picture

This looks good to me and I plan to commit it once we are out of code freeze. Thanks!

  • xjm committed c7f4700 on 8.7.x
    Issue #3028490 by tim.plunkett, Kristen Pol, xjm, tedbow, r.aubin,...

  • xjm committed f10cf17 on 8.6.x
    Issue #3028490 by tim.plunkett, Kristen Pol, xjm, tedbow, r.aubin,...
xjm’s picture

Committed to 8.7.x ad 8.6.x, thanks!

Does the bug exist in 8.5.x? (Since this is a security issue there too probably.)

tim.plunkett’s picture

Version: 8.6.x-dev » 8.5.x-dev
StatusFileSize
new1.83 KB
new9.48 KB

Here's a backport, the bug does indeed exist in 8.5

tedbow’s picture

Patch in #48 looks good. Would RTBC but already there

The last submitted patch, 48: 3028490-access-48-FAIL.patch, failed testing. View results

  • xjm committed 2838553 on 8.5.x
    Issue #3028490 by tim.plunkett, Kristen Pol, xjm, tedbow, r.aubin,...
xjm’s picture

Status: Reviewed & tested by the community » Fixed
Issue tags: -8.6.10 release notes +8.6.11 release notes

Committed to 8.5.x. Thanks!

Status: Fixed » Closed (fixed)

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