Skip to content
Permalink
Browse files Browse the repository at this point in the history
Improvements to cookie value validation (#508)
  • Loading branch information
Luke Towers committed Jul 29, 2020
1 parent af1d519 commit 28310d4
Show file tree
Hide file tree
Showing 2 changed files with 120 additions and 0 deletions.
47 changes: 47 additions & 0 deletions src/Cookie/CookieValuePrefix.php
@@ -0,0 +1,47 @@
<?php namespace October\Rain\Cookie;

/**
* Helper class to prefix, unprefix, and verify cookie values
*/
class CookieValuePrefix
{
/**
* Create a new cookie value prefix for the given cookie name.
*
* @param string $name The name of the cookie
* @param string $key The encryption key
* @return string
*/
public static function create($name, $key)
{
return hash_hmac('sha1', $name . 'v2', $key) . '|';
}

/**
* Remove the cookie value prefix.
*
* @param string $cookieValue
* @return string
*/
public static function remove($cookieValue)
{
return substr($cookieValue, 41);
}

/**
* Verify the provided cookie's value
*
* @param string $name The name of the cookie
* @param string $value The decrypted value of the cookie to be verified
* @param string $key The encryption key used to encrypt the cookie originally
* @return string|null $verifiedValue The unprefixed value if it passed verification, otherwise null
*/
public static function getVerifiedValue($name, $value, $key)
{
$verifiedValue = null;
if (starts_with($value, static::create($name, $key))) {
$verifiedValue = static::remove($value);
}
return $verifiedValue;
}
}
73 changes: 73 additions & 0 deletions src/Cookie/Middleware/EncryptCookies.php
@@ -1,6 +1,11 @@
<?php namespace October\Rain\Cookie\Middleware;

use Config;
use Session;
use October\Rain\Cookie\CookieValuePrefix;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Contracts\Encryption\DecryptException;
use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;
use Illuminate\Cookie\Middleware\EncryptCookies as EncryptCookiesBase;

Expand Down Expand Up @@ -57,4 +62,72 @@ protected function decryptArray(array $cookie)

return $decrypted;
}

/**
* Decrypt the cookies on the request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* @return \Symfony\Component\HttpFoundation\Request
*/
protected function decrypt(Request $request)
{
foreach ($request->cookies as $key => $cookie) {
if ($this->isDisabled($key)) {
continue;
}

try {
// Decrypt the request-provided cookie
$decryptedValue = $this->decryptCookie($key, $cookie);

// Verify that the decrypted value belongs to this cookie key, use null if it fails
$value = CookieValuePrefix::getVerifiedValue($key, $decryptedValue, $this->encrypter->getKey());

/**
* If the cookie is for the session and the value is a valid Session ID,
* then allow it to pass through even if the validation failed (most likely
* because the upgrade just occurred)
*
* The cookie will be adjusted on the next request
* @todo Remove if year >= 2021 or build >= 475
*/
if (empty($value) && $key === Config::get('session.cookie') && Session::isValidId($decryptedValue)) {
$value = $decryptedValue;
}

// Set the verified cookie value on the request
$request->cookies->set($key, $value);
} catch (DecryptException $e) {
$request->cookies->set($key, null);
}
}

return $request;
}

/**
* Encrypt the cookies on an outgoing response.
*
* @param \Symfony\Component\HttpFoundation\Response $response
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function encrypt(Response $response)
{
foreach ($response->headers->getCookies() as $cookie) {
if ($this->isDisabled($cookie->getName())) {
continue;
}

$response->headers->setCookie($this->duplicate(
$cookie,
$this->encrypter->encrypt(
// Prefix the cookie value to verify that it belongs to the current cookie
CookieValuePrefix::create($cookie->getName(), $this->encrypter->getKey()) . $cookie->getValue(),
static::serialized($cookie->getName())
)
));
}

return $response;
}
}

0 comments on commit 28310d4

Please sign in to comment.