Skip to content

Possibility of Reintroducing HS256/RSA256 Type Confusion (CVE-2021-46743) #351

Closed
@paragonie-security

Description

@paragonie-security

This is a follow-up to the HS256/RS256 Type Confusion attack against the JWT protocol.

Now, firebase/php-jwt attempts to side-step this risk by forcing the user to hard-code the algorithms they wish to support.

php-jwt/src/JWT.php

Lines 103 to 108 in d2113d9

if (empty(static::$supported_algs[$header->alg])) {
throw new UnexpectedValueException('Algorithm not supported');
}
if (!\in_array($header->alg, $allowed_algs)) {
throw new UnexpectedValueException('Algorithm not allowed');
}

If $key is an array, and $header contains a kid field, the key used to verify a token is determined by the kid header.

php-jwt/src/JWT.php

Lines 114 to 123 in d2113d9

if (\is_array($key) || $key instanceof \ArrayAccess) {
if (isset($header->kid)) {
if (!isset($key[$header->kid])) {
throw new UnexpectedValueException('"kid" invalid, unable to lookup correct key');
}
$key = $key[$header->kid];
} else {
throw new UnexpectedValueException('"kid" empty, unable to lookup correct key');
}
}

Reintroducing the Vulnerability

EDIT: 2021-08-11 - This example is a bit misleading. See the attached Proof of Concept file instead. php-jwt-poc.zip


Let's say you're a service that wants to check HS256 tokens against one key type and RS256 tokens against another. Your HS256 key has {"kid":"gandalf0"}, while your RS256 public key has {"kid":"legolas1"}.

You might call php-jwt like so:

<?php
$validated = JWT::decode(
    $attackerControlledString,
    [
        'galdalf0' => '256-bit key goes here',
        'legolas1' => 'RSA public key goes here' 
    ],
    ['RS256', 'HS256']
);

If anyone ever sets up JWT like this:

Congratulations! you've just reintroduced the critical vulnerability in your usage of the app.

All you have to do is set {"alg":"HS256","kid":"legolas1"} and use the SHA256 hash of the RSA public key as an HMAC key, and you can mint tokens all day long.

Another Way To Setup This Vulnerability

Let's say you have two different endpoints that only each accept one JWT signature algorithm.

  • /oauth only allows RS256 tokens
  • /rpc only allows HS256 tokens

This is clearly a safer usage of the firebase/php-jwt library than the previous canned example.

Suppose you have a universal array that your application uses that maps keys--as is common with PHP frameworks.

<?php
return [
        'galdalf0' => '256-bit key goes here',
        'legolas1' => 'RSA public key goes here' 
    ];

In this setup, once again, you've introduced a critical vulnerability into your application.

All an attacker needs to do is target your /rpc endpoint and swap the Key ID from gandalf0 to legolas1 and they can mint tokens.

What's going on here?

The fundamental problem is that the keys passed to firebase/php-jwt are just strings. This flies in the face of cryptography engineering best practices: A key should always be considered to be the raw key material alongside its parameter choices.

Is this a security vulnerability?

This is not a vulnerability in the firebase/php-jwt library. It is, however, a very sharp edge that an unsuspecting developer could cut themselves on.

Cryptography should be easy to use, hard to misuse, and secure by default.

Whether the JOSE authors want to acknowledge it or not, what they published was a cryptographic protocol--one that fails to live up to these tenets. It's worth noting that PASETO mitigates this in its specification, so library authors don't have to even worry about it.

Any application that uses this library in the way described above has a critical vulnerability, so it may be prudent to publish a security advisory and/or obtain a CVE identifier. Update: This was assigned CVE-2021-46743

The good news is: This can be easily fixed.

The bad news is: It constitutes a backwards compatibility break.

How to Fix This Library

If you were to update the API to require keys to be a Keyring object, which maps a string KeyID (kid) to a JWTKey object--and that JWTKey object had a hard-coded algorithm that it could be used with--then this issue would be easily avoided.

Pseudocode

<?php
class JWTKey {
    protected string $alg;
    protected string $keyMaterial;
    public function __construct(string $keyMaterial, string $alg) {}
    public function isValidFor(string $headerAlg): bool
    {
         return hash_equals($this->alg, $headerAlg);
    }
    public function getKeyMaterial(): string 
    {
         return $this->keyMaterial;
    }
    public function __toString()
    {
        return $this->keyMaterial;
    }
}
<?php
declare(strict_types=1);
final class Keyring implements ArrayAccess {
    /** @var array<string, JWTKey> $mapping */
    private array $mapping;

    public function mapKeyId(string $keyId, JWTKey $key): self
    {
         $this->mapping[$keyId] = $key;
         return $this;
    }

    public offsetExists($offset): bool {
        return array_key_exists($offset, $this->mapping);
    }
    public offsetGet($offset): JWTKey {
        return $this->mapping[$offset];
    }
    public offsetSet($offset, $value): void {
         $this->mapKeyId($offset, $value);
    }
    public offsetUnset(mixed $offset): void {
         unset($this->mapping[$offset]);
    }
}
-     public static function decode($jwt, $key, array $allowed_algs = array())
+     public static function decode($jwt, JWTKey|Keyring $key, array $allowed_algs = array())
-         if (\is_array($key) || $key instanceof \ArrayAccess) {
+         if ($key instanceof Keyring) {
              if (isset($header->kid)) {
+         if (!$key->isValidFor($header->alg)) {
+             throw new UnexpectedValueException('This key cannot be used with ' . $header->alg);
+         }
          // Check the signature
          if (!static::verify("$headb64.$bodyb64", $sig, $key, $header->alg)) {

Edited to clarify the value of a security advisory and/or CVE to ensure users of this library remain safe against attack.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions