Commit e772c905 authored by bojanz's avatar bojanz

Fix access bypass vulnerability.

parent 79094476
......@@ -19,13 +19,14 @@ use Drupal\Core\Field\BaseFieldDefinition;
* plural = "@count logs",
* ),
* handlers = {
* "access" = "Drupal\commerce\EmbeddedEntityAccessControlHandler",
* "access" = "Drupal\commerce_log\LogAccessControlHandler",
* "list_builder" = "Drupal\commerce_log\LogListBuilder",
* "storage" = "Drupal\commerce_log\LogStorage",
* "view_builder" = "Drupal\commerce_log\LogViewBuilder",
* "views_data" = "Drupal\commerce\CommerceEntityViewsData",
* },
* base_table = "commerce_log",
* internal = TRUE,
* entity_keys = {
* "id" = "log_id",
* "uuid" = "uuid",
......
<?php
namespace Drupal\commerce_log;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an access control handler for logs.
*
* Logs are internal entities, always managed and viewed in the context
* of their source entity. The source entity access is used when possible:
* - A log can be viewed if the source entity can be viewed.
* - A log can be updated or deleted if the source entity can be updated.
*
* Note: There are currently no limitations imposed on log creation.
*/
class LogAccessControlHandler extends CoreEntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($account->hasPermission($this->entityType->getAdminPermission())) {
return AccessResult::allowed()->cachePerPermissions();
}
/** @var \Drupal\commerce_log\Entity\LogInterface $entity */
$source_entity = $entity->getSourceEntity();
if (!$source_entity) {
// The log is malformed.
return AccessResult::forbidden()->addCacheableDependency($entity);
}
$parent_operation = ($operation == 'view') ? 'view' : 'update';
$result = $source_entity->access($parent_operation, $account, TRUE);
return $result;
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowed();
}
}
<?php
namespace Drupal\Tests\commerce_log\Kernel;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_log\Entity\Log;
use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
/**
* Tests the log access control.
*
* @coversDefaultClass \Drupal\commerce_log\LogAccessControlHandler
* @group commerce
*/
class LogAccessTest extends CommerceKernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'entity_reference_revisions',
'profile',
'state_machine',
'commerce_log',
'commerce_order',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('profile');
$this->installEntitySchema('commerce_order');
$this->installEntitySchema('commerce_order_item');
$this->installEntitySchema('commerce_log');
$this->installConfig('commerce_order');
// Create uid: 1 here so that it's skipped in test cases.
$admin_user = $this->createUser();
}
/**
* @covers ::checkAccess
*/
public function testAccess() {
$order = Order::create([
'type' => 'default',
'state' => 'canceled',
]);
$order->save();
/** @var \Drupal\commerce_log\Entity\LogInterface $log */
$log = Log::create([
'category_id' => 'commerce_order',
'template_id' => 'order_canceled',
'source_entity_id' => $order->id(),
'source_entity_type' => 'commerce_order',
'params' => [],
]);
$log->save();
$account = $this->createUser([], ['access administration pages']);
$this->assertFalse($log->access('view', $account));
$this->assertFalse($log->access('update', $account));
$this->assertFalse($log->access('delete', $account));
$account = $this->createUser([], ['view commerce_order']);
$this->assertTrue($log->access('view', $account));
$this->assertFalse($log->access('update', $account));
$this->assertFalse($log->access('delete', $account));
$account = $this->createUser([], ['update commerce_order']);
$this->assertFalse($log->access('view', $account));
$this->assertTrue($log->access('update', $account));
$this->assertTrue($log->access('delete', $account));
$account = $this->createUser([], ['administer commerce_order']);
$this->assertTrue($log->access('view', $account));
$this->assertTrue($log->access('update', $account));
$this->assertTrue($log->access('delete', $account));
// Broken source reference.
$log->set('source_entity_id', '999');
$account = $this->createUser([], ['update commerce_order']);
$this->assertFalse($log->access('view', $account));
$this->assertFalse($log->access('update', $account));
$this->assertFalse($log->access('delete', $account));
}
}
......@@ -211,3 +211,27 @@ function commerce_order_post_update_8() {
$field->setLocked(FALSE);
$field->save();
}
/**
* Grants the "manage order items" permission to roles that can update orders.
*/
function commerce_order_post_update_9() {
$entity_type_manager = \Drupal::entityTypeManager();
/** @var \Drupal\commerce_order\Entity\OrderItemTypeInterface[] $order_item_types */
$order_item_types = $entity_type_manager->getStorage('commerce_order_item_type')->loadMultiple();
/** @var \Drupal\user\RoleInterface[] $roles */
$roles = $entity_type_manager->getStorage('user_role')->loadMultiple();
$order_type_storage = $entity_type_manager->getStorage('commerce_order_type');
foreach ($roles as $role) {
foreach ($order_item_types as $order_item_type) {
$order_type = $order_type_storage->load($order_item_type->getOrderTypeId());
// If the role can update the order type, then it can also manage the
// order items of this bundle.
if ($role->hasPermission("update {$order_type->id()} commerce_order")) {
$role->grantPermission("manage {$order_item_type->id()} commerce_order_item");
}
}
$role->save();
}
}
......@@ -22,11 +22,12 @@ use Drupal\Core\Field\BaseFieldDefinition;
* singular = "@count order item",
* plural = "@count order items",
* ),
* bundle_label = @Translation("order item type"),
* bundle_label = @Translation("Order item type"),
* handlers = {
* "event" = "Drupal\commerce_order\Event\OrderItemEvent",
* "storage" = "Drupal\commerce_order\OrderItemStorage",
* "access" = "Drupal\commerce\EmbeddedEntityAccessControlHandler",
* "access" = "Drupal\commerce_order\OrderItemAccessControlHandler",
* "permission_provider" = "Drupal\commerce_order\OrderItemPermissionProvider",
* "views_data" = "Drupal\commerce_order\OrderItemViewsData",
* "form" = {
* "default" = "Drupal\Core\Entity\ContentEntityForm",
......
<?php
namespace Drupal\commerce_order;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Provides an access control handler for order items.
*
* Order items are always managed in the scope of their parent (the order),
* so they have a simplified permission set, and rely on parent access
* when possible:
* - An order item can be viewed if the parent order can be viewed.
* - An order item can be created, updated or deleted if the user has the
* "manage $bundle commerce_order_item" permission.
*
* The "administer commerce_order" permission is also respected.
*/
class OrderItemAccessControlHandler extends CoreEntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($account->hasPermission($this->entityType->getAdminPermission())) {
return AccessResult::allowed()->cachePerPermissions();
}
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $entity */
$order = $entity->getOrder();
if (!$order) {
// The order item is malformed.
return AccessResult::forbidden()->addCacheableDependency($entity);
}
if ($operation == 'view') {
$result = $order->access('view', $account, TRUE);
}
else {
$bundle = $entity->bundle();
$result = AccessResult::allowedIfHasPermission($account, "manage $bundle commerce_order_item");
}
return $result;
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
// Create access depends on the "manage" permission because the full entity
// is not passed, making it impossible to determine the parent order.
$result = AccessResult::allowedIfHasPermissions($account, [
$this->entityType->getAdminPermission(),
"manage $entity_bundle commerce_order_item",
], 'OR');
return $result;
}
}
<?php
namespace Drupal\commerce_order;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\entity\EntityPermissionProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides permissions for order items.
*/
class OrderItemPermissionProvider implements EntityPermissionProviderInterface, EntityHandlerInterface {
use StringTranslationTrait;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* Constructs a new OrderItemPermissionProvider object.
*
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
* The entity type bundle info.
*/
public function __construct(EntityTypeBundleInfoInterface $entity_type_bundle_info) {
$this->entityTypeBundleInfo = $entity_type_bundle_info;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$container->get('entity_type.bundle.info')
);
}
/**
* {@inheritdoc}
*/
public function buildPermissions(EntityTypeInterface $entity_type) {
$entity_type_id = $entity_type->id();
$bundles = $this->entityTypeBundleInfo->getBundleInfo($entity_type_id);
$permissions = [];
foreach ($bundles as $bundle_name => $bundle_info) {
// The title is in a different format than the order type permissions,
// to differentiate order types from order item types.
$permissions["manage {$bundle_name} {$entity_type_id}"] = [
'title' => $this->t('[Order items] Manage %bundle', [
'%bundle' => $bundle_info['label'],
]),
'provider' => 'commerce_order',
];
}
return $permissions;
}
}
<?php
namespace Drupal\Tests\commerce_order\Kernel;
use Drupal\commerce_order\Entity\Order;
use Drupal\commerce_order\Entity\OrderItem;
use Drupal\commerce_order\Entity\OrderItemType;
use Drupal\commerce_price\Price;
use Drupal\Tests\commerce\Kernel\CommerceKernelTestBase;
/**
* Tests the order item access control.
*
* @coversDefaultClass \Drupal\commerce_order\OrderItemAccessControlHandler
* @group commerce
*/
class OrderItemAccessTest extends CommerceKernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
'entity_reference_revisions',
'profile',
'state_machine',
'commerce_order',
];
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installEntitySchema('profile');
$this->installEntitySchema('commerce_order');
$this->installEntitySchema('commerce_order_item');
$this->installConfig('commerce_order');
OrderItemType::create([
'id' => 'test',
'label' => 'Test',
'orderType' => 'default',
])->save();
// Create uid: 1 here so that it's skipped in test cases.
$admin_user = $this->createUser();
}
/**
* @covers ::checkAccess
*/
public function testAccess() {
/** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
$order_item = OrderItem::create([
'type' => 'test',
'quantity' => 2,
'unit_price' => new Price('12.00', 'USD'),
]);
$order_item->save();
$order = Order::create([
'type' => 'default',
'state' => 'canceled',
'order_items' => [$order_item],
]);
$order->save();
$order_item = $this->reloadEntity($order_item);
$account = $this->createUser([], ['access administration pages']);
$this->assertFalse($order_item->access('view', $account));
$this->assertFalse($order_item->access('update', $account));
$this->assertFalse($order_item->access('delete', $account));
$account = $this->createUser([], ['view commerce_order']);
$this->assertTrue($order_item->access('view', $account));
$this->assertFalse($order_item->access('update', $account));
$this->assertFalse($order_item->access('delete', $account));
$account = $this->createUser([], ['update commerce_order']);
$this->assertFalse($order_item->access('view', $account));
$this->assertFalse($order_item->access('update', $account));
$this->assertFalse($order_item->access('delete', $account));
$account = $this->createUser([], [
'manage test commerce_order_item',
]);
$this->assertFalse($order_item->access('view', $account));
$this->assertTrue($order_item->access('update', $account));
$this->assertTrue($order_item->access('delete', $account));
$account = $this->createUser([], ['administer commerce_order']);
$this->assertTrue($order_item->access('view', $account));
$this->assertTrue($order_item->access('update', $account));
$this->assertTrue($order_item->access('delete', $account));
// Broken order reference.
$order_item->set('order_id', '999');
$account = $this->createUser([], ['manage test commerce_order_item']);
$this->assertFalse($order_item->access('view', $account));
$this->assertFalse($order_item->access('update', $account));
$this->assertFalse($order_item->access('delete', $account));
}
/**
* @covers ::checkCreateAccess
*/
public function testCreateAccess() {
$access_control_handler = \Drupal::entityTypeManager()->getAccessControlHandler('commerce_order_item');
$account = $this->createUser([], ['access content']);
$this->assertFalse($access_control_handler->createAccess('test', $account));
$account = $this->createUser([], ['administer commerce_order']);
$this->assertTrue($access_control_handler->createAccess('test', $account));
$account = $this->createUser([], ['manage test commerce_order_item']);
$this->assertTrue($access_control_handler->createAccess('test', $account));
}
}
......@@ -138,3 +138,27 @@ function commerce_product_post_update_5() {
}
}
}
/**
* Grants the "manage variations" permission to roles that can update products.
*/
function commerce_product_post_update_6() {
$entity_type_manager = \Drupal::entityTypeManager();
/** @var \Drupal\commerce_product\Entity\ProductTypeInterface[] $product_types */
$product_types = $entity_type_manager->getStorage('commerce_product_type')->loadMultiple();
/** @var \Drupal\user\RoleInterface[] $roles */
$roles = $entity_type_manager->getStorage('user_role')->loadMultiple();
foreach ($roles as $role) {
foreach ($product_types as $product_type) {
// If the role had any update permission, grant the manage permission.
if (
$role->hasPermission("update any {$product_type->id()} commerce_product") ||
$role->hasPermission("update own {$product_type->id()} commerce_product")
) {
$role->grantPermission("manage {$product_type->getVariationTypeId()} commerce_product_variation");
}
}
$role->save();
}
}
......@@ -23,7 +23,7 @@ use Drupal\Core\Field\BaseFieldDefinition;
* handlers = {
* "event" = "Drupal\commerce_product\Event\ProductAttributeValueEvent",
* "storage" = "Drupal\commerce_product\ProductAttributeValueStorage",
* "access" = "Drupal\commerce\EmbeddedEntityAccessControlHandler",
* "access" = "Drupal\commerce_product\ProductAttributeValueAccessControlHandler",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "views_data" = "Drupal\commerce\CommerceEntityViewsData",
* "translation" = "Drupal\content_translation\ContentTranslationHandler"
......
......@@ -30,7 +30,8 @@ use Drupal\user\UserInterface;
* handlers = {
* "event" = "Drupal\commerce_product\Event\ProductVariationEvent",
* "storage" = "Drupal\commerce_product\ProductVariationStorage",
* "access" = "Drupal\commerce\EmbeddedEntityAccessControlHandler",
* "access" = "Drupal\commerce_product\ProductVariationAccessControlHandler",
* "permission_provider" = "Drupal\commerce_product\ProductVariationPermissionProvider",
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
* "views_data" = "Drupal\commerce\CommerceEntityViewsData",
* "form" = {
......
<?php
namespace Drupal\commerce_product;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides an access control handler for product attribute values.
*
* Product attribute values are always managed in the scope of their parent
* (the product attribute), so the parent access is used when possible:
* - A product attribute value can be created, updated or deleted if the
* parent can be updated.
* - A product attribute value can be viewed by any user with the
* "access content" permission, to allow rendering on any product.
* This matches the logic used by taxonomy terms.
*/
class ProductAttributeValueAccessControlHandler extends CoreEntityAccessControlHandler implements EntityHandlerInterface {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a new ProductAttributeValueAccessControlHandler object.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type definition.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(EntityTypeInterface $entity_type, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($entity_type);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('entity_type.manager')
);
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($account->hasPermission($this->entityType->getAdminPermission())) {
return AccessResult::allowed()->cachePerPermissions();
}
if ($operation == 'view') {
$result = AccessResult::allowedIfHasPermission($account, 'access content');
}
else {
/** @var \Drupal\commerce_product\Entity\ProductAttributeValueInterface $entity */
$result = $entity->getAttribute()->access('update', $account, TRUE);
}
return $result;
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
if ($account->hasPermission($this->entityType->getAdminPermission())) {
return AccessResult::allowed()->cachePerPermissions();
}
$product_attribute_storage = $this->entityTypeManager->getStorage('commerce_product_attribute');
$product_attribute = $product_attribute_storage->create([
'id' => $entity_bundle,
]);
$result = $product_attribute->access('update', $account, TRUE);
return $result;
}
}
<?php
namespace Drupal\commerce_product;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler as CoreEntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**