How Magento Generates Admin Secret URL Keys

Published: February 11, 2018

Tags:

Recently, while looking into a vulnerability for the Magento Bug Bounty I needed to generate the secret key for an admin URL. While I’d long known that Magento adds these keys for security purposes (specifically to prevent against CSRF attacks) I never understood how exactly these keys are generated. In this post, I’ll document my findings.

How It Works

Magento 2

NOTE: This below is based on the Magento 2 code base as of version 2.2.2.

The issue I was investigating was in Magento 2, so that is where I first looked into this. I quickly found my answer in Magento\Backend\Model\Url’s getSecretKey method. Below is the full method…

/**
 * Generate secret key for controller and action based on form key
 *
 * @param string $routeName
 * @param string $controller Controller name
 * @param string $action Action name
 * @return string
 */
public function getSecretKey($routeName = null, $controller = null, $action = null)
{
    $salt = $this->formKey->getFormKey();
    $request = $this->_getRequest();
    if (!$routeName) {
        if ($request->getBeforeForwardInfo('route_name') !== null) {
            $routeName = $request->getBeforeForwardInfo('route_name');
        } else {
            $routeName = $request->getRouteName();
        }
    }
    if (!$controller) {
        if ($request->getBeforeForwardInfo('controller_name') !== null) {
            $controller = $request->getBeforeForwardInfo('controller_name');
        } else {
            $controller = $request->getControllerName();
        }
    }
    if (!$action) {
        if ($request->getBeforeForwardInfo('action_name') !== null) {
            $action = $request->getBeforeForwardInfo('action_name');
        } else {
            $action = $request->getActionName();
        }
    }
    $secret = $routeName . $controller . $action . $salt;
    return $this->_encryptor->getHash($secret);
}

The important parts are here…

$salt = $this->formKey->getFormKey();

$secret = $routeName . $controller . $action . $salt;
return $this->_encryptor->getHash($secret);

As you can see it concatenates the $routeName, $controller, $action (e.g. salesorderindex) and $salt (which is the form key for current session) and passes it to the encryptor to get a hash. But what does the $this->_encryptor->getHash() do…?

For that we need to look at Magento\Framework\Encryption\Encryptor::getHash(). Here it is…

/**
 * @inheritdoc
 */
public function getHash($password, $salt = false, $version = self::HASH_VERSION_LATEST)
{
    if ($salt === false) {
        return $this->hash($password);
    }
    if ($salt === true) {
        $salt = self::DEFAULT_SALT_LENGTH;
    }
    if (is_integer($salt)) {
        $salt = $this->random->getRandomString($salt);
    }

    return implode(
        self::DELIMITER,
        [
            $this->hash($salt . $password),
            $salt,
            $version
        ]
    );
}

$salt if not passed so it hit’s this branch…

return $this->hash($password);

Next we have to look at Magento\Framework\Encryption\Encryptor::hash()

public function hash($data, $version = self::HASH_VERSION_LATEST)
{
    return hash($this->hashVersionMap[$version], $data);
}

$version is not passed so it defaults to self::HASH_VERSION_LATEST, which is set to 1…

const HASH_VERSION_LATEST = 1

Looking at $hashVersionMap we can see that means it will use sha256…

private $hashVersionMap = [
    self::HASH_VERSION_MD5 => 'md5',
    self::HASH_VERSION_SHA256 => 'sha256'
];

So, putting it all together, generation basically boils down to this…

$secret = hash('sha256', $module . $controller . $action . $formKey);

Magento 1

NOTE: This below is based on the Magento 1 code base as of version 1.9.3.7.

Magento 1 is more or less the same, but with a few key differences. The equivalent to Magento 2’s Magento\Backend\Model\Url::getSecretKey() in Magento 1 is Mage_Adminhtml_Model_Url::getSecretKey(). Here it is…

public function getSecretKey($controller = null, $action = null)
{
    $salt = Mage::getSingleton('core/session')->getFormKey();
    $p = explode('/', trim($this->getRequest()->getOriginalPathInfo(), '/'));
    if (!$controller) {
        $controller = !empty($p[1]) ? $p[1] : $this->getRequest()->getControllerName();
    }
    if (!$action) {
        $action = !empty($p[2]) ? $p[2] : $this->getRequest()->getActionName();
    }
    $secret = $controller . $action . $salt;
    return Mage::helper('core')->getHash($secret);
}

The main notable difference is that the module name is not included, meaning that if two modules have the same controller / action they will use the same secret key.

Also, if you look at Mage_Core_Model_Encryption::hash() you’ll see that Magento 1 uses md5 instead of sha256.

public function hash($data)
{
    return md5($data);
}

Other than that, secret keys in Magento 1 are generated exactly as they are in Magento 2.

Max Chadwick Hi, I'm Max!

I'm a software developer who mainly works in PHP, but loves dabbling in other languages like Go and Ruby. Technical topics that interest me are monitoring, security and performance. I'm also a stickler for good documentation and clear technical writing.

During the day I lead a team of developers and solve challenging technical problems at Rightpoint where I mainly work with the Magento platform. I've also spoken at a number of events.

In my spare time I blog about tech, work on open source and participate in bug bounty programs.

If you'd like to get in contact, you can find me on Twitter and LinkedIn.