Skip to content

Commit c46efd2

Browse files
authored
Merge pull request #6867 from magento-cia/MC-41512
[cia] MC-41512: [PSIRT-16607] PII leak of all purchases including guest ones due to an IDOR allowing the attacker to change any cart's customer id
2 parents f423622 + 090d4fb commit c46efd2

File tree

7 files changed

+185
-13
lines changed

7 files changed

+185
-13
lines changed

app/code/Magento/Quote/Test/Mftf/Metadata/CustomerCartItemMeta.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
<operations xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1010
xsi:noNamespaceSchemaLocation="urn:magento:mftf:DataGenerator/etc/dataOperation.xsd">
1111

12-
<operation name="CreateCustomerCartItem" dataType="CustomerCartItem" type="create" auth="adminOauth" url="/V1/carts/mine/items" method="POST">
12+
<operation name="CreateCustomerCartItem" dataType="CustomerCartItem" type="create" auth="adminOauth" url="/V1/carts/{quote_id}/items" method="POST">
1313
<contentType>application/json</contentType>
1414
<object key="cartItem" dataType="CustomerCartItem">
1515
<field key="quote_id" type="string">string</field>

app/code/Magento/Quote/etc/webapi.xml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
<resource ref="self" />
6666
</resources>
6767
<data>
68-
<parameter name="cartId" force="true">%cart_id%</parameter>
68+
<parameter name="quote.id" force="true">%cart_id%</parameter>
6969
</data>
7070
</route>
7171
<route url="/V1/carts/mine/order" method="PUT">
@@ -237,7 +237,7 @@
237237
<resource ref="self" />
238238
</resources>
239239
<data>
240-
<parameter name="cartId" force="true">%cart_id%</parameter>
240+
<parameter name="cartItem.quote_id" force="true">%cart_id%</parameter>
241241
</data>
242242
</route>
243243
<route url="/V1/carts/mine/items/:itemId" method="PUT">
@@ -246,7 +246,7 @@
246246
<resource ref="self" />
247247
</resources>
248248
<data>
249-
<parameter name="cartId" force="true">%cart_id%</parameter>
249+
<parameter name="cartItem.quote_id" force="true">%cart_id%</parameter>
250250
</data>
251251
</route>
252252
<route url="/V1/carts/mine/items/:itemId" method="DELETE">

app/code/Magento/Webapi/Controller/Rest/InputParamsResolver.php

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
namespace Magento\Webapi\Controller\Rest;
88

9+
use Magento\Framework\Api\SimpleDataObjectConverter;
10+
use Magento\Framework\App\ObjectManager;
11+
use Magento\Framework\Reflection\MethodsMap;
12+
use Magento\Framework\Webapi\Exception;
913
use Magento\Framework\Webapi\ServiceInputProcessor;
1014
use Magento\Framework\Webapi\Rest\Request as RestRequest;
1115
use Magento\Webapi\Controller\Rest\Router\Route;
@@ -45,6 +49,11 @@ class InputParamsResolver
4549
*/
4650
private $requestValidator;
4751

52+
/**
53+
* @var MethodsMap
54+
*/
55+
private $methodsMap;
56+
4857
/**
4958
* Initialize dependencies
5059
*
@@ -53,26 +62,30 @@ class InputParamsResolver
5362
* @param ServiceInputProcessor $serviceInputProcessor
5463
* @param Router $router
5564
* @param RequestValidator $requestValidator
65+
* @param MethodsMap|null $methodsMap
5666
*/
5767
public function __construct(
5868
RestRequest $request,
5969
ParamsOverrider $paramsOverrider,
6070
ServiceInputProcessor $serviceInputProcessor,
6171
Router $router,
62-
RequestValidator $requestValidator
72+
RequestValidator $requestValidator,
73+
MethodsMap $methodsMap = null
6374
) {
6475
$this->request = $request;
6576
$this->paramsOverrider = $paramsOverrider;
6677
$this->serviceInputProcessor = $serviceInputProcessor;
6778
$this->router = $router;
6879
$this->requestValidator = $requestValidator;
80+
$this->methodsMap = $methodsMap ?: ObjectManager::getInstance()
81+
->get(MethodsMap::class);
6982
}
7083

7184
/**
7285
* Process and resolve input parameters
7386
*
7487
* @return array
75-
* @throws \Magento\Framework\Webapi\Exception
88+
* @throws Exception
7689
*/
7790
public function resolve()
7891
{
@@ -81,6 +94,7 @@ public function resolve()
8194
$serviceMethodName = $route->getServiceMethod();
8295
$serviceClassName = $route->getServiceClass();
8396
$inputData = $this->getInputData();
97+
8498
return $this->serviceInputProcessor->process($serviceClassName, $serviceMethodName, $inputData);
8599
}
86100

@@ -108,6 +122,7 @@ public function getInputData()
108122
} else {
109123
$inputData = $this->request->getRequestData();
110124
}
125+
$this->validateParameters($serviceClassName, $serviceMethodName, array_keys($route->getParameters()));
111126

112127
return $this->paramsOverrider->override($inputData, $route->getParameters());
113128
}
@@ -122,6 +137,46 @@ public function getRoute()
122137
if (!$this->route) {
123138
$this->route = $this->router->match($this->request);
124139
}
140+
125141
return $this->route;
126142
}
143+
144+
/**
145+
* Validate that parameters are really used in the current request.
146+
*
147+
* @param string $serviceClassName
148+
* @param string $serviceMethodName
149+
* @param array $paramOverriders
150+
*/
151+
private function validateParameters(
152+
string $serviceClassName,
153+
string $serviceMethodName,
154+
array $paramOverriders
155+
): void {
156+
$methodParams = $this->methodsMap->getMethodParams($serviceClassName, $serviceMethodName);
157+
foreach ($paramOverriders as $key => $param) {
158+
$arrayKeys = explode('.', $param);
159+
$value = array_shift($arrayKeys);
160+
161+
foreach ($methodParams as $serviceMethodParam) {
162+
$serviceMethodParamName = $serviceMethodParam[MethodsMap::METHOD_META_NAME];
163+
$serviceMethodType = $serviceMethodParam[MethodsMap::METHOD_META_TYPE];
164+
165+
$camelCaseValue = SimpleDataObjectConverter::snakeCaseToCamelCase($value);
166+
if ($serviceMethodParamName === $value || $serviceMethodParamName === $camelCaseValue) {
167+
if (count($arrayKeys) > 0) {
168+
$camelCaseKey = SimpleDataObjectConverter::snakeCaseToCamelCase('set_' . $arrayKeys[0]);
169+
$this->validateParameters($serviceMethodType, $camelCaseKey, [implode('.', $arrayKeys)]);
170+
}
171+
unset($paramOverriders[$key]);
172+
break;
173+
}
174+
}
175+
}
176+
if (!empty($paramOverriders)) {
177+
throw new \UnexpectedValueException(
178+
__('The current request does not expect the next parameters: ' . implode(', ', $paramOverriders))
179+
);
180+
}
181+
}
127182
}

dev/tests/api-functional/_files/Magento/TestModule1/etc/webapi.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@
7272
<resource ref="Magento_TestModule1::resource2" />
7373
</resources>
7474
</route>
75+
<route method="PUT" url="/V1/testmodule1/withParam">
76+
<service class="Magento\TestModule1\Service\V1\AllSoapAndRestInterface" method="update" />
77+
<resources>
78+
<resource ref="Magento_TestModule1::resource1" />
79+
<resource ref="Magento_TestModule1::resource2" />
80+
</resources>
81+
<data>
82+
<parameter name="paramId" force="true">%param_id%</parameter>
83+
</data>
84+
</route>
7585
<route method="GET" url="/V2/testmodule1/:id">
7686
<service class="Magento\TestModule1\Service\V2\AllSoapAndRestInterface" method="item" />
7787
<resources>

dev/tests/api-functional/testsuite/Magento/Webapi/Routing/CoreRoutingTest.php

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,21 @@
99
*/
1010
namespace Magento\Webapi\Routing;
1111

12+
use Magento\Framework\Webapi\Rest\Request;
13+
use Magento\Integration\Model\Integration;
14+
use Magento\TestFramework\Authentication\OauthHelper;
1215
use Magento\TestFramework\Helper\Bootstrap;
1316
use Magento\TestFramework\TestCase\Webapi\Adapter\Rest\RestClient;
1417

15-
class CoreRoutingTest extends \Magento\Webapi\Routing\BaseService
18+
class CoreRoutingTest extends BaseService
1619
{
1720
public function testBasicRoutingExplicitPath()
1821
{
1922
$itemId = 1;
2023
$serviceInfo = [
2124
'rest' => [
2225
'resourcePath' => '/V1/testmodule1/' . $itemId,
23-
'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET,
26+
'httpMethod' => Request::HTTP_METHOD_GET,
2427
],
2528
'soap' => [
2629
'service' => 'testModule1AllSoapAndRestV1',
@@ -38,7 +41,7 @@ public function testDisabledIntegrationAuthorizationException()
3841
$serviceInfo = [
3942
'rest' => [
4043
'resourcePath' => '/V1/testmodule1/' . $itemId,
41-
'httpMethod' => \Magento\Framework\Webapi\Rest\Request::HTTP_METHOD_GET,
44+
'httpMethod' => Request::HTTP_METHOD_GET,
4245
],
4346
'soap' => [
4447
'service' => 'testModule1AllSoapAndRestV1',
@@ -48,11 +51,11 @@ public function testDisabledIntegrationAuthorizationException()
4851
$requestData = ['itemId' => $itemId];
4952

5053
/** Disable integration associated with active OAuth credentials. */
51-
$credentials = \Magento\TestFramework\Authentication\OauthHelper::getApiAccessCredentials();
52-
/** @var \Magento\Integration\Model\Integration $integration */
54+
$credentials = OauthHelper::getApiAccessCredentials();
55+
/** @var Integration $integration */
5356
$integration = $credentials['integration'];
5457
$originalStatus = $integration->getStatus();
55-
$integration->setStatus(\Magento\Integration\Model\Integration::STATUS_INACTIVE)->save();
58+
$integration->setStatus(Integration::STATUS_INACTIVE)->save();
5659

5760
try {
5861
$this->assertUnauthorizedException($serviceInfo, $requestData);
@@ -83,9 +86,37 @@ public function testRestNoAcceptHeader()
8386
$this->_markTestAsRestOnly();
8487
/** @var $curlClient RestClient */
8588
$curlClient = Bootstrap::getObjectManager()->get(
86-
\Magento\TestFramework\TestCase\Webapi\Adapter\Rest\RestClient::class
89+
RestClient::class
8790
);
8891
$response = $curlClient->get('/V1/testmodule1/resource1/1', [], ['Accept:']);
8992
$this->assertEquals('testProduct1', $response['name'], "Empty Accept header failed to return response.");
9093
}
94+
95+
/**
96+
* Verifies that exception is thrown when the request contains unexpected parameters.
97+
*
98+
* @return void
99+
*/
100+
public function testRequestParamsUnexpectedValueException(): void
101+
{
102+
$this->_markTestAsRestOnly();
103+
$expectedMessage = "Internal Error. Details are available in Magento log file. Report ID: webapi-";
104+
$unexpectedMessage = "\"%fieldName\" is required. Enter and try again.";
105+
106+
$serviceInfo = [
107+
'rest' => [
108+
'resourcePath' => '/V1/testmodule1/withParam',
109+
'httpMethod' => Request::HTTP_METHOD_PUT,
110+
],
111+
];
112+
113+
try {
114+
$this->_webApiCall($serviceInfo);
115+
} catch (\Exception $e) {
116+
$exceptionResult = $this->processRestExceptionResult($e);
117+
$actualMessage = $exceptionResult['message'];
118+
$this->assertStringNotContainsString($unexpectedMessage, $actualMessage);
119+
$this->assertStringContainsString($expectedMessage, $actualMessage);
120+
}
121+
}
91122
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Catalog\Api\ProductRepositoryInterface;
9+
use Magento\Customer\Api\AccountManagementInterface;
10+
use Magento\Customer\Api\CustomerRepositoryInterface;
11+
use Magento\Quote\Api\CartRepositoryInterface;
12+
use Magento\Quote\Model\Quote\AddressFactory;
13+
use Magento\Quote\Model\QuoteFactory;
14+
use Magento\Store\Api\WebsiteRepositoryInterface;
15+
use Magento\TestFramework\Helper\Bootstrap;
16+
use Magento\TestFramework\Workaround\Override\Fixture\Resolver;
17+
18+
Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_addresses.php');
19+
Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products.php');
20+
21+
$objectManager = Bootstrap::getObjectManager();
22+
23+
/** Import Customer Address Data */
24+
$quoteShippingAddress = $objectManager->get(AddressFactory::class)->create();
25+
$customerRepository = $objectManager->get(CustomerRepositoryInterface::class);
26+
$customer = $customerRepository->get('customer_with_addresses@test.com');
27+
$addresses = $customer->getAddresses();
28+
$addressFirst = array_shift($addresses);
29+
$quoteShippingAddress->importCustomerAddressData($addressFirst);
30+
31+
$productRepository = $objectManager->create(ProductRepositoryInterface::class);
32+
$product = $productRepository->get('simple');
33+
34+
$websiteRepository = $objectManager->get(WebsiteRepositoryInterface::class);
35+
$mainWebsite = $websiteRepository->get('base');
36+
37+
$accountManagement = $objectManager->create(AccountManagementInterface::class);
38+
39+
$quoteRepository = $objectManager->get(CartRepositoryInterface::class);
40+
$quote = $objectManager->get(QuoteFactory::class)->create();
41+
$quote->setStoreId($mainWebsite->getDefaultStore()->getId())
42+
->setIsActive(true)
43+
->setIsMultiShipping(false)
44+
->assignCustomerWithAddressChange($customer)
45+
->setShippingAddress($quoteShippingAddress)
46+
->setBillingAddress($quoteShippingAddress)
47+
->setCheckoutMethod('customer')
48+
->setPasswordHash($accountManagement->getPasswordHash('password'))
49+
->setReservedOrderId('test_order_999')
50+
->setCustomerEmail('aaa2@aaa.com')
51+
->addProduct($product, 2);
52+
$quoteRepository->save($quote);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
use Magento\Framework\Api\SearchCriteriaBuilder;
9+
use Magento\Quote\Api\CartRepositoryInterface;
10+
use Magento\TestFramework\Helper\Bootstrap;
11+
use Magento\TestFramework\Workaround\Override\Fixture\Resolver;
12+
13+
Resolver::getInstance()->requireDataFixture('Magento/Customer/_files/customer_with_addresses_rollback.php');
14+
Resolver::getInstance()->requireDataFixture('Magento/Catalog/_files/products_rollback.php');
15+
16+
$objectManager = Bootstrap::getObjectManager();
17+
$searchCriteriaBuilder = $objectManager->get(SearchCriteriaBuilder::class);
18+
$searchCriteria = $searchCriteriaBuilder->addFilter('reserved_order_id', 'test_order_999')->create();
19+
20+
/** @var CartRepositoryInterface $quoteRepository */
21+
$quoteRepository = $objectManager->get(CartRepositoryInterface::class);
22+
$items = $quoteRepository->getList($searchCriteria)->getItems();
23+
$item = array_pop($items);
24+
$quoteRepository->delete($item);

0 commit comments

Comments
 (0)