CVE-2020-7070 php: PHP parses encoded cookie names so malicious `__Host-` cookies can be sent

Publication Date2020-06-14
SeverityLow
TypeCross-Site Request Forgery
Affected PHP Versions
  • 7.2.0 - 7.2.33
  • 7.3.0 - 7.3.22
  • 7.4.0 - 7.4.10

CVE Details

In PHP versions 7.2.x below 7.2.34, 7.3.x below 7.3.23, and 7.4.x below 7.4.11, when PHP is processing incoming HTTP cookie values, the cookie names are url-decoded. This may lead to cookies with prefixes like __Host being confused with cookies that decode to such a prefix, thus leading to an attacker being able to forge cookie which is supposed to be secure.

HTTP cookies are generally considered insecure and not to be trusted. However, the cookie specification allows for "cookie prefixes" to cookie names in order to indicate browser attributes; such names are generally trustworthy, and can only be set by the browser itself. Two in particular are commonly used:

  • __Secure- must only be used when a page was transported over HTTPS, and must be set with the "secure" flag.
  • __Host- must only be used when a page was transported over HTTPS, must be set with the "secure" flag, must not have a domain specified (and cannot be sent to subdomains), and the path must be /.

The PHP cookie parser assumes cookie values are urlencoded, and thus urldecodes them during parsing. However, in PHP versions 7.2.x below 7.2.34, 7.3.x below 7.3.23, and 7.4.x below 7.4.11, it not only urldecoded the values, but also the names. This latter could lead to an attacker urlencoding the name such that it would decode to a cookie prefixed name. This can have security implications if the value is, for instance, a session ID, as a session ID could be re-used in a subdomain, despite being locked to the root domain via the originally set __Host- prefixed cookie.

Recommendations

Upgrade to PHP 7.2.34 or later, 7.3.23 or later, or 7.4.11 or later.

If you cannot upgrade, and you rely on __Host- or __Secure- prefixed cookies to prevent CSRF or XSS vulnerabilities, you may want to consider parsing the Cookie header manually:

function getCookies(): array
{
    $headers = getallheaders();
    $cookies = [];

    $cookieHeader = null;
    foreach (getallheaders() as $header => $value) {
        if (preg_match('/^cookie$/i', $header)) {
            $cookieHeader = $value;
        }
    }

    if ($cookieHeader === null) {
        return [];
    }

    $cookiePairs = preg_split('/\s*[;]\s*/', $cookieHeader);
    $cookiePairs = array_filter(is_array($cookiePairs) ? $cookiePairs : []);

    foreach ($cookiePairs as $pair) {
        $parts = explode('=', $pair, 2);
        $name = array_shift($parts);
        $value = count($parts) ? array_shift($value) : null;
        $cookies[$name] = is_string($value) ? urldecode($value) : null;
    }

    return $cookies;
}