How Magento Generates Admin Secret URL Keys
Published: February 11, 2018
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.
	Hi, I'm Max!