CVE-2021-41106: Insufficient Verification of Data Authenticity (CWE-345)
value: JWT is a library to work with JSON Web Token and JSON Web Signature. Prior to versions 3.4.6, 4.0.4, and 4.1.5, users of HMAC-based algorithms (HS256, HS384, and HS512) combined with `Lcobucci\JWT\Signer\Key\LocalFileReference` as key are having their tokens issued/validated using the file path as hashing key - instead of the contents. The HMAC hashing functions take any string as input and, since users can issue and validate tokens, users are lead to believe that everything works properly. Versions 3.4.6, 4.0.4, and 4.1.5 have been patched to always load the file contents, deprecated the `Lcobucci\JWT\Signer\Key\LocalFileReference`, and suggest `Lcobucci\JWT\Signer\Key\InMemory` as the alternative. As a workaround, use `Lcobucci\JWT\Signer\Key\InMemory` instead of `Lcobucci\JWT\Signer\Key\LocalFileReference` to create the instances of one's keys.
CWE description: The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.
CVSS v3 base score: 3.3
Published: 2021-09-28T21:15Z
Repository: https://github.com/lcobucci/jwt
Language: PHP
Files changed: docs/configuration.md, docs/upgrading.md, phpstan.neon.dist, src/Signer/Key/LocalFileReference.php, test/functional/HmacTokenTest.php, test/unit/Signer/EcdsaTest.php, test/unit/Signer/Key/LocalFileReferenceTest.php, test/unit/Signer/RsaTest.php
Commit date: 2021-09-27 15:36:39 +0200
Commit message: Ensure key contents is used for all hashing algorithms

On v3.4.0 we introduced the
`Lcobucci\JWT\Signer\Key\LocalFileReference`, which was designed to be
used with OpenSSL-based algorithms and avoid having to load the file
contents into user-land to sign/verify tokens.

However, other algorithms don't understand the scheme `file://` and will
incorrectly use the file path as the signing key.

This modifies and deprecates `LocalFileReference` to avoid that
misleading behaviour.

Co-authored-by: Anton Smirnov <sandfox@sandfox.me>
Co-authored-by: Marco Pivetta <ocramius@gmail.com>

Diff: diff --git a/docs/configuration.md b/docs/configuration.md
index 3afcf67..b0c9f05 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -39,14 +39,12 @@ $key = InMemory::base64Encoded('YSB2ZXJ5IGxvbmcgYSB2ZXJ5IHVsdHJhIHNlY3VyZSBrZXkg
 
 Or provide a file path:
 
 ```php
 use Lcobucci\JWT\Signer\Key\InMemory;
-use Lcobucci\JWT\Signer\Key\LocalFileReference;
 
 $key = InMemory::file(__DIR__ . '/path-to-my-key-stored-in-a-file.pem'); // this reads the file and keeps its contents in memory
-$key = LocalFileReference::file(__DIR__ . '/path-to-my-key-stored-in-a-file.pem'); // this just keeps a reference to file
 ```
 
 #### For symmetric algorithms
 
 Symmetric algorithms use the same key for both signature creation and verification.
@@ -76,17 +74,16 @@ Asymmetric algorithms use a **private key** for signature creation and a **publi
 This means that it's fine to distribute your **public key**. However, the **private key** should **remain secret**.
 
 ```php
 use Lcobucci\JWT\Configuration;
 use Lcobucci\JWT\Signer;
-use Lcobucci\JWT\Signer\Key\LocalFileReference;
 use Lcobucci\JWT\Signer\Key\InMemory;
 
 $configuration = Configuration::forAsymmetricSigner(
     // You may use RSA or ECDSA and all their variations (256, 384, and 512)
     new Signer\Rsa\Sha256(),
-    LocalFileReference::file(__DIR__ . '/my-private-key.pem'),
+    InMemory::file(__DIR__ . '/my-private-key.pem'),
     InMemory::base64Encoded('mBC5v1sOKVvbdEitdSBenu59nfNfhwkedkJVNabosTw=')
     // You may also override the JOSE encoder/decoder if needed by providing extra arguments here
 );
 ```
 
diff --git a/docs/upgrading.md b/docs/upgrading.md
index 7610e45..fa070d8 100644
--- a/docs/upgrading.md
+++ b/docs/upgrading.md
@@ -94,13 +94,12 @@ Check an example of how to migrate the injection of builder+signer+key to config
 You can find more information on how to use the configuration object, [here](configuration.md).
 
 ### Use new `Key` objects
 
 `Lcobucci\JWT\Signer\Key` has been converted to an interface in `v4.0`.
-We provide two new implementations: `Lcobucci\JWT\Signer\Key\InMemory` and `Lcobucci\JWT\Signer\Key\LocalFileReference`.
 
-`Lcobucci\JWT\Signer\Key\InMemory` is a drop-in replacement of the behaviour for `Lcobucci\JWT\Signer\Key` in `v3.x`.
+We provide `Lcobucci\JWT\Signer\Key\InMemory`, a drop-in replacement of the behaviour for `Lcobucci\JWT\Signer\Key` in `v3.x`.
 You will need to pick the appropriated named constructor to migrate your code:
 
 ```diff
  <?php
  declare(strict_types=1);
diff --git a/phpstan.neon.dist b/phpstan.neon.dist
index 0cd9727..2d6cbac 100644
--- a/phpstan.neon.dist
+++ b/phpstan.neon.dist
@@ -4,5 +4,6 @@ parameters:
         - src
         - test
 
     ignoreErrors:
         - '#.*OpenSSLAsymmetricKey.*#'
+        - '#Call to method .* of deprecated class Lcobucci\\JWT\\Signer\\Key\\LocalFileReference#'
diff --git a/src/Signer/Key/LocalFileReference.php b/src/Signer/Key/LocalFileReference.php
index 4663e9e..b6cb3f1 100644
--- a/src/Signer/Key/LocalFileReference.php
+++ b/src/Signer/Key/LocalFileReference.php
@@ -7,16 +7,18 @@ use Lcobucci\JWT\Signer\Key;
 
 use function file_exists;
 use function strpos;
 use function substr;
 
+/** @deprecated please use {@see InMemory} instead */
 final class LocalFileReference implements Key
 {
     private const PATH_PREFIX = 'file://';
 
     private string $path;
     private string $passphrase;
+    private string $contents;
 
     private function __construct(string $path, string $passphrase)
     {
         $this->path       = $path;
         $this->passphrase = $passphrase;
@@ -36,11 +38,15 @@ final class LocalFileReference implements Key
         return new self($path, $passphrase);
     }
 
     public function contents(): string
     {
-        return self::PATH_PREFIX . $this->path;
+        if (! isset($this->contents)) {
+            $this->contents = InMemory::file($this->path)->contents();
+        }
+
+        return $this->contents;
     }
 
     public function passphrase(): string
     {
         return $this->passphrase;
diff --git a/test/functional/HmacTokenTest.php b/test/functional/HmacTokenTest.php
index d87771a..a7a9a34 100644
--- a/test/functional/HmacTokenTest.php
+++ b/test/functional/HmacTokenTest.php
@@ -5,16 +5,21 @@ namespace Lcobucci\JWT\FunctionalTests;
 
 use Lcobucci\JWT\Configuration;
 use Lcobucci\JWT\Signer\Hmac\Sha256;
 use Lcobucci\JWT\Signer\Hmac\Sha512;
 use Lcobucci\JWT\Signer\Key\InMemory;
+use Lcobucci\JWT\Signer\Key\LocalFileReference;
 use Lcobucci\JWT\Token;
 use Lcobucci\JWT\Validation\Constraint\SignedWith;
 use Lcobucci\JWT\Validation\RequiredConstraintsViolated;
 use PHPUnit\Framework\TestCase;
 
 use function assert;
+use function file_put_contents;
+use function is_string;
+use function sys_get_temp_dir;
+use function tempnam;
 
 /**
  * @covers \Lcobucci\JWT\Configuration
  * @covers \Lcobucci\JWT\Encoding\JoseEncoder
  * @covers \Lcobucci\JWT\Encoding\ChainedFormatter
@@ -129,6 +134,41 @@ class HmacTokenTest extends TestCase
         $constraint = new SignedWith($this->config->signer(), $this->config->verificationKey());
 
         self::assertTrue($this->config->validator()->validate($token, $constraint));
         self::assertEquals('world', $token->claims()->get('hello'));
     }
+
+    /** @test */
+    public function signatureValidationWithLocalFileKeyReferenceWillOperateWithKeyContents(): void
+    {
+        $key = tempnam(sys_get_temp_dir(), 'key');
+        assert(is_string($key));
+
+        file_put_contents($key, 'just a dummy key');
+
+        $validKey      = LocalFileReference::file($key);
+        $invalidKey    = InMemory::plainText('file://' . $key);
+        $signer        = new Sha256();
+        $configuration = Configuration::forSymmetricSigner($signer, $validKey);
+        $validator     = $configuration->validator();
+
+        $token = $configuration->builder()
+            ->withClaim('foo', 'bar')
+            ->getToken($configuration->signer(), $configuration->signingKey());
+
+        self::assertFalse(
+            $validator->validate(
+                $token,
+                new SignedWith($signer, $invalidKey)
+            ),
+            'Token cannot be validated against the **path** of the key'
+        );
+
+        self::assertTrue(
+            $validator->validate(
+                $token,
+                new SignedWith($signer, $validKey)
+            ),
+            'Token can be validated against the **contents** of the key'
+        );
+    }
 }
diff --git a/test/unit/Signer/EcdsaTest.php b/test/unit/Signer/EcdsaTest.php
index 4354ec0..eb9c958 100644
--- a/test/unit/Signer/EcdsaTest.php
+++ b/test/unit/Signer/EcdsaTest.php
@@ -54,10 +54,11 @@ final class EcdsaTest extends TestCase
      * @covers \Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter
      * @covers \Lcobucci\JWT\Signer\OpenSSL
      *
      * @uses \Lcobucci\JWT\Signer\Ecdsa::__construct
      * @uses \Lcobucci\JWT\Signer\Key\LocalFileReference
+     * @uses \Lcobucci\JWT\Signer\Key\InMemory
      */
     public function signShouldReturnTheAHashBasedOnTheOpenSslSignature(): void
     {
         $payload = 'testing';
 
@@ -86,10 +87,11 @@ final class EcdsaTest extends TestCase
      * @covers \Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter
      * @covers \Lcobucci\JWT\Signer\OpenSSL
      *
      * @uses \Lcobucci\JWT\Signer\Ecdsa::__construct
      * @uses \Lcobucci\JWT\Signer\Key\LocalFileReference
+     * @uses \Lcobucci\JWT\Signer\Key\InMemory
      */
     public function verifyShouldDelegateToEcdsaSignerUsingPublicKey(): void
     {
         $payload    = 'testing';
         $privateKey = openssl_pkey_get_private(self::$ecdsaKeys['private']->contents());
diff --git a/test/unit/Signer/Key/LocalFileReferenceTest.php b/test/unit/Signer/Key/LocalFileReferenceTest.php
index 05e088d..e4aa501 100644
--- a/test/unit/Signer/Key/LocalFileReferenceTest.php
+++ b/test/unit/Signer/Key/LocalFileReferenceTest.php
@@ -4,11 +4,15 @@ declare(strict_types=1);
 namespace Lcobucci\JWT\Signer\Key;
 
 use org\bovigo\vfs\vfsStream;
 use PHPUnit\Framework\TestCase;
 
-/** @coversDefaultClass \Lcobucci\JWT\Signer\Key\LocalFileReference */
+/**
+ * @coversDefaultClass \Lcobucci\JWT\Signer\Key\LocalFileReference
+ *
+ * @uses \Lcobucci\JWT\Signer\Key\InMemory
+ */
 final class LocalFileReferenceTest extends TestCase
 {
     /** @before */
     public function configureRootDir(): void
     {
@@ -31,35 +35,21 @@ final class LocalFileReferenceTest extends TestCase
         $this->expectExceptionMessage('The path "vfs://root/test2.pem" does not contain a valid key file');
 
         LocalFileReference::file(vfsStream::url('root/test2.pem'));
     }
 
-    /**
-     * @test
-     *
-     * @covers ::file
-     * @covers ::__construct
-     * @covers ::contents
-     */
-    public function pathShouldBeNormalised(): void
-    {
-        $key = LocalFileReference::file('file://' . vfsStream::url('root/test.pem'));
-
-        self::assertSame('file://vfs://root/test.pem', $key->contents());
-    }
-
     /**
      * @test
      *
      * @covers ::file
      * @covers ::__construct
      * @covers ::contents
      * @covers ::passphrase
      */
-    public function contentsShouldReturnOnlyTheReferenceToTheFile(): void
+    public function pathShouldBeNormalised(): void
     {
-        $key = LocalFileReference::file(vfsStream::url('root/test.pem'), 'test');
+        $key = LocalFileReference::file('file://' . vfsStream::url('root/test.pem'), 'test');
 
-        self::assertSame('file://vfs://root/test.pem', $key->contents());
+        self::assertSame('testing', $key->contents());
         self::assertSame('test', $key->passphrase());
     }
 }
diff --git a/test/unit/Signer/RsaTest.php b/test/unit/Signer/RsaTest.php
index 9909b11..3dd504e 100644
--- a/test/unit/Signer/RsaTest.php
+++ b/test/unit/Signer/RsaTest.php
@@ -28,10 +28,11 @@ final class RsaTest extends TestCase
      * @covers ::sign
      * @covers ::keyType
      * @covers \Lcobucci\JWT\Signer\OpenSSL
      *
      * @uses \Lcobucci\JWT\Signer\Key\LocalFileReference
+     * @uses \Lcobucci\JWT\Signer\Key\InMemory
      */
     public function signShouldReturnAValidOpensslSignature(): void
     {
         $payload = 'testing';
 
@@ -98,10 +99,11 @@ KEY;
      * @covers ::keyType
      * @covers \Lcobucci\JWT\Signer\OpenSSL
      * @covers \Lcobucci\JWT\Signer\InvalidKeyProvided
      *
      * @uses \Lcobucci\JWT\Signer\Key\LocalFileReference
+     * @uses \Lcobucci\JWT\Signer\Key\InMemory
      */
     public function signShouldRaiseAnExceptionWhenKeyTypeIsNotRsa(): void
     {
         $signer = $this->getSigner();
 
@@ -117,10 +119,11 @@ KEY;
      * @covers ::verify
      * @covers ::keyType
      * @covers \Lcobucci\JWT\Signer\OpenSSL
      *
      * @uses \Lcobucci\JWT\Signer\Key\LocalFileReference
+     * @uses \Lcobucci\JWT\Signer\Key\InMemory
      */
     public function verifyShouldReturnTrueWhenSignatureIsValid(): void
     {
         $payload    = 'testing';
         $privateKey = openssl_pkey_get_private(self::$rsaKeys['private']->contents());
@@ -159,10 +162,11 @@ KEY;
      * @covers ::verify
      * @covers \Lcobucci\JWT\Signer\OpenSSL
      * @covers \Lcobucci\JWT\Signer\InvalidKeyProvided
      *
      * @uses \Lcobucci\JWT\Signer\Key\LocalFileReference
+     * @uses \Lcobucci\JWT\Signer\Key\InMemory
      */
     public function verifyShouldRaiseAnExceptionWhenKeyTypeIsNotRsa(): void
     {
         $signer = $this->getSigner();
 

Vulnerable code: use Lcobucci\JWT\Signer\Key\LocalFileReference;
$key = LocalFileReference::file(__DIR__ . '/path-to-my-key-stored-in-a-file.pem'); // this just keeps a reference to file
use Lcobucci\JWT\Signer\Key\LocalFileReference;
    LocalFileReference::file(__DIR__ . '/my-private-key.pem'),
We provide two new implementations: `Lcobucci\JWT\Signer\Key\InMemory` and `Lcobucci\JWT\Signer\Key\LocalFileReference`.
`Lcobucci\JWT\Signer\Key\InMemory` is a drop-in replacement of the behaviour for `Lcobucci\JWT\Signer\Key` in `v3.x`.
        return self::PATH_PREFIX . $this->path;
/** @coversDefaultClass \Lcobucci\JWT\Signer\Key\LocalFileReference */
    /**
     * @test
     *
     * @covers ::file
     * @covers ::__construct
     * @covers ::contents
     */
    public function pathShouldBeNormalised(): void
    {
        $key = LocalFileReference::file('file://' . vfsStream::url('root/test.pem'));

        self::assertSame('file://vfs://root/test.pem', $key->contents());
    }

    public function contentsShouldReturnOnlyTheReferenceToTheFile(): void
        $key = LocalFileReference::file(vfsStream::url('root/test.pem'), 'test');
        self::assertSame('file://vfs://root/test.pem', $key->contents());
Fixed code:     InMemory::file(__DIR__ . '/my-private-key.pem'),
We provide `Lcobucci\JWT\Signer\Key\InMemory`, a drop-in replacement of the behaviour for `Lcobucci\JWT\Signer\Key` in `v3.x`.
        - '#Call to method .* of deprecated class Lcobucci\\JWT\\Signer\\Key\\LocalFileReference#'
/** @deprecated please use {@see InMemory} instead */
    private string $contents;
        if (! isset($this->contents)) {
            $this->contents = InMemory::file($this->path)->contents();
        }

        return $this->contents;
use Lcobucci\JWT\Signer\Key\LocalFileReference;
use function file_put_contents;
use function is_string;
use function sys_get_temp_dir;
use function tempnam;

    /** @test */
    public function signatureValidationWithLocalFileKeyReferenceWillOperateWithKeyContents(): void
    {
        $key = tempnam(sys_get_temp_dir(), 'key');
        assert(is_string($key));

        file_put_contents($key, 'just a dummy key');

        $validKey      = LocalFileReference::file($key);
        $invalidKey    = InMemory::plainText('file://' . $key);
        $signer        = new Sha256();
        $configuration = Configuration::forSymmetricSigner($signer, $validKey);
        $validator     = $configuration->validator();

        $token = $configuration->builder()
            ->withClaim('foo', 'bar')
            ->getToken($configuration->signer(), $configuration->signingKey());

        self::assertFalse(
            $validator->validate(
                $token,
                new SignedWith($signer, $invalidKey)
            ),
            'Token cannot be validated against the **path** of the key'
        );

        self::assertTrue(
            $validator->validate(
                $token,
                new SignedWith($signer, $validKey)
            ),
            'Token can be validated against the **contents** of the key'
        );
    }
     * @uses \Lcobucci\JWT\Signer\Key\InMemory
     * @uses \Lcobucci\JWT\Signer\Key\InMemory
/**
 * @coversDefaultClass \Lcobucci\JWT\Signer\Key\LocalFileReference
 *
 * @uses \Lcobucci\JWT\Signer\Key\InMemory
 */
    public function pathShouldBeNormalised(): void
        $key = LocalFileReference::file('file://' . vfsStream::url('root/test.pem'), 'test');
        self::assertSame('testing', $key->contents());
     * @uses \Lcobucci\JWT\Signer\Key\InMemory
     * @uses \Lcobucci\JWT\Signer\Key\InMemory
     * @uses \Lcobucci\JWT\Signer\Key\InMemory
     * @uses \Lcobucci\JWT\Signer\Key\InMemory