VYPR
Critical severityNVD Advisory· Published Jun 3, 2025· Updated Apr 15, 2026

CVE-2025-48951

CVE-2025-48951

Description

Auth0-PHP is a PHP SDK for Auth0 Authentication and Management APIs. Versions 8.0.0-BETA3 prior to 8.3.1 contain a vulnerability due to insecure deserialization of cookie data. If exploited, since SDKs process cookie content without prior authentication, a threat actor could send a specially crafted cookie containing malicious serialized data. Applications using the Auth0-PHP SDK are affected, as are applications using the Auth0/symfony, Auth0/laravel-auth0, or Auth0/wordpress SDKs, because those SDKsrely on the Auth0-PHP SDK versions from 8.0.0-BETA3 until 8.14.0. Version 8.3.1 contains a patch for the issue.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
auth0/auth0-phpPackagist
>= 8.0.0-BETA3, < 8.3.18.3.1

Patches

2
04b1f5daa8bd

[SDK-3646] Reliability and performance improvements to CookieStore (#649)

https://github.com/auth0/auth0-PHPEvan SimsSep 24, 2022via ghsa
5 files changed · +181 110
  • composer.json+1 1 modified
    @@ -50,7 +50,7 @@
         "pestphp/pest": "^1.21",
         "php-http/mock-client": "^1.5",
         "phpstan/phpstan": "^1.7",
    -    "phpstan/phpstan-strict-rules": "^1.3",
    +    "phpstan/phpstan-strict-rules": "1.4.3",
         "phpunit/phpunit": "^9.5",
         "rector/rector": "^0.13.6",
         "squizlabs/php_codesniffer": "^3.7",
    
  • src/Store/CookieStore.php+121 71 modified
    @@ -14,9 +14,8 @@
      */
     final class CookieStore implements StoreInterface
     {
    -    public const USE_CRYPTO = true;
         public const KEY_HASHING_ALGO = 'sha256';
    -    public const KEY_CHUNKING_THRESHOLD = 3072;
    +    public const KEY_CHUNKING_THRESHOLD = 2048;
         public const KEY_SEPARATOR = '_';
         public const VAL_CRYPTO_ALGO = 'aes-128-gcm';
     
    @@ -48,6 +47,16 @@ final class CookieStore implements StoreInterface
          */
         private bool $deferring = false;
     
    +    /**
    +     * Determine if changes have been made since the last setState.
    +     */
    +    private bool $dirty = false;
    +
    +    /**
    +     * Determine if changes have been made since the last setState.
    +     */
    +    private bool $encrypt = true;
    +
         /**
          * CookieStore constructor.
          *
    @@ -68,13 +77,7 @@ public function __construct(
     
             $this->configuration = $configuration;
             $this->namespace = (string) $namespace;
    -
    -        // @phpstan-ignore-next-line
    -        if (self::USE_CRYPTO) {
    -            $this->namespace = hash(self::KEY_HASHING_ALGO, $this->namespace);
    -        }
    -
    -        $this->threshold = self::KEY_CHUNKING_THRESHOLD - strlen($this->namespace) + 2;
    +        $this->threshold = self::KEY_CHUNKING_THRESHOLD - strlen($this->namespace);
     
             $this->getState();
         }
    @@ -95,6 +98,25 @@ public function getThreshold(): int
             return $this->threshold;
         }
     
    +    /**
    +     * Returns the current encryption state
    +     */
    +    public function getEncrypted(): bool
    +    {
    +        return $this->encrypt;
    +    }
    +
    +    /**
    +     * Toggle the encryption state
    +     *
    +     * @param bool $encrypt Enable or disable cookie encryption.
    +     */
    +    public function setEncrypted(bool $encrypt = true): self
    +    {
    +        $this->encrypt = $encrypt;
    +        return $this;
    +    }
    +
         /**
          * Defer saving state changes to destination to improve performance during blocks of changes.
          *
    @@ -103,14 +125,13 @@ public function getThreshold(): int
         public function defer(
             bool $deferring
         ): void {
    +        $this->deferring = $deferring;
    +
             // If we were deferring state saving and we've been asked to cancel that deference
    -        if ($this->deferring && ! $deferring) {
    +        if (! $deferring) {
                 // Immediately push the state to the host device.
                 $this->setState();
             }
    -
    -        // Update our deference state.
    -        $this->deferring = $deferring;
         }
     
         /**
    @@ -125,6 +146,10 @@ public function getState(
         ): array {
             // Overwrite our internal state with one passed (presumably during unit tests.)
             if ($state !== null) {
    +            if ($this->store !== $state) {
    +                $this->dirty = true;
    +            }
    +
                 return $this->store = $state;
             }
     
    @@ -151,7 +176,7 @@ public function getState(
             }
     
             // If no cookies were found, set an empty state and continue.
    -        if (mb_strlen($data) === 0) {
    +        if ($data === '') {
                 return $this->store = [];
             }
     
    @@ -171,9 +196,16 @@ public function getState(
     
         /**
          * Push our storage state to the source for persistence.
    +     *
    +     * @psalm-suppress UnusedFunctionCall
          */
    -    public function setState(): self
    -    {
    +    public function setState(
    +        bool $force = false
    +    ): self {
    +        if (!$this->dirty && !$force) {
    +            return $this;
    +        }
    +
             $setOptions = $this->getCookieOptions();
             $deleteOptions = $this->getCookieOptions(-1000);
             $existing = [];
    @@ -212,7 +244,7 @@ public function setState(): self
                         // @codeCoverageIgnoreStart
                         if (! defined('AUTH0_TESTS_DIR')) {
                             /** @var array{expires?: int, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: 'Lax'|'lax'|'None'|'none'|'Strict'|'strict', url_encode?: int} $setOptions */
    -                        setcookie($cookieName, $chunk, $setOptions);
    +                        setrawcookie($cookieName, $chunk, $setOptions);
                         }
                         // @codeCoverageIgnoreEnd
     
    @@ -233,14 +265,15 @@ public function setState(): self
                 // @codeCoverageIgnoreStart
                 if (! defined('AUTH0_TESTS_DIR')) {
                     /** @var array{expires?: int, path?: string, domain?: string, secure?: bool, httponly?: bool, samesite?: 'Lax'|'lax'|'None'|'none'|'Strict'|'strict', url_encode?: int} $deleteOptions */
    -                setcookie($cookieName, '', $deleteOptions);
    +                setrawcookie($cookieName, '', $deleteOptions);
                 }
                 // @codeCoverageIgnoreEnd
     
                 // Clear PHP's internal COOKIE global of the orphaned cookie.
                 unset($_COOKIE[$cookieName]);
             }
     
    +        $this->dirty = false;
             return $this;
         }
     
    @@ -260,7 +293,10 @@ public function set(
                 [$key, \Auth0\SDK\Exception\ArgumentException::missing('key')],
             ])->isString();
     
    -        $this->store[(string) $key] = $value;
    +        if (! isset($this->store[(string) $key]) || $this->store[(string) $key] !== $value) {
    +            $this->store[(string) $key] = $value;
    +            $this->dirty = true;
    +        }
     
             if (! $this->deferring) {
                 $this->setState();
    @@ -303,7 +339,10 @@ public function delete(
                 [$key, \Auth0\SDK\Exception\ArgumentException::missing('key')],
             ])->isString();
     
    -        unset($this->store[(string) $key]);
    +        if (isset($this->store[(string) $key])) {
    +            unset($this->store[(string) $key]);
    +            $this->dirty = true;
    +        }
     
             if (! $this->deferring) {
                 $this->setState();
    @@ -315,7 +354,10 @@ public function delete(
          */
         public function purge(): void
         {
    -        $this->store = [];
    +        if ($this->store !== []) {
    +            $this->store = [];
    +            $this->dirty = true;
    +        }
     
             if (! $this->deferring) {
                 $this->setState();
    @@ -351,10 +393,9 @@ public function getCookieOptions(
                 $options['samesite'] = 'Lax';
             }
     
    -        $domain = $this->configuration->getCookieDomain() ?? $_SERVER['HTTP_HOST'] ?? null;
    +        $domain = $this->configuration->getCookieDomain() ?? null;
     
    -        if ($domain !== null) {
    -            /** @var string $domain */
    +        if ($domain !== null && $domain !== $_SERVER['HTTP_HOST']) {
                 $options['domain'] = $domain;
             }
     
    @@ -364,47 +405,70 @@ public function getCookieOptions(
         /**
          * Encrypt data for safe storage format for a cookie.
          *
    -     * @param array<mixed> $data Data to encrypt.
    +     * @param array<mixed> $data    Data to encrypt.
    +     * @param array<mixed> $options Additional configuration options.
          *
          * @psalm-suppress TypeDoesNotContainType
          */
    -    private function encrypt(
    -        array $data
    +    public function encrypt(
    +        array $data,
    +        array $options = []
         ): string {
    -        // @codeCoverageIgnoreStart
    -        // @phpstan-ignore-next-line
    -        if (! self::USE_CRYPTO) {
    -            return base64_encode(json_encode(serialize($data), JSON_THROW_ON_ERROR));
    +        if (! $this->encrypt) {
    +            $data = $options['encoded1'] ?? json_encode($data);
    +
    +            if (! is_string($data)) {
    +                return '';
    +            }
    +
    +            return rawurlencode($data);
             }
    -        // @codeCoverageIgnoreEnd
     
             $secret = $this->configuration->getCookieSecret();
    -        $ivLen = openssl_cipher_iv_length(self::VAL_CRYPTO_ALGO);
    +        $ivLen = $options['ivLen'] ?? openssl_cipher_iv_length(self::VAL_CRYPTO_ALGO);
    +        $tag = null;
     
             if ($secret === null) {
                 throw \Auth0\SDK\Exception\ConfigurationException::requiresCookieSecret();
             }
     
    -        // @codeCoverageIgnoreStart
    -        if ($ivLen === false) {
    +        if (! is_int($ivLen)) {
    +            return '';
    +        }
    +
    +        $iv = $options['iv'] ?? openssl_random_pseudo_bytes($ivLen);
    +
    +        if (! is_string($iv)) {
    +            return '';
    +        }
    +
    +        $data = $options['encoded1'] ?? json_encode($data);
    +
    +        if (! is_string($data)) {
                 return '';
             }
    -        // @codeCoverageIgnoreEnd
     
    -        $iv = openssl_random_pseudo_bytes($ivLen);
    +        // Encrypt the PHP array.
    +        $encrypted = $options['encrypted'] ?? openssl_encrypt($data, self::VAL_CRYPTO_ALGO, $secret, 0, $iv, $tag);
    +        $iv = $options['iv'] ?? $iv;
    +        $tag = $options['tag'] ?? $tag;
     
    -        // @codeCoverageIgnoreStart
    -        // @phpstan-ignore-next-line
    -        if ($iv === false) {
    +        if (! is_string($encrypted)) {
                 return '';
             }
    -        // @codeCoverageIgnoreEnd
     
    -        // Encrypt the serialized PHP array.
    -        $encrypted = openssl_encrypt(serialize($data), self::VAL_CRYPTO_ALGO, $secret, 0, $iv, $tag);
    +        if (! is_string($tag)) {
    +            return '';
    +        }
     
             // Return a JSON encoded object containing the crypto tag and iv, and the encrypted data.
    -        return json_encode(serialize(['tag' => base64_encode($tag), 'iv' => base64_encode($iv), 'data' => $encrypted]), JSON_THROW_ON_ERROR);
    +        $encoded = $options['encoded2'] ?? json_encode(['tag' => base64_encode($tag), 'iv' => base64_encode($iv), 'data' => $encrypted]);
    +
    +        if (is_string($encoded)) {
    +            return rawurlencode($encoded);
    +        }
    +
    +        return '';
         }
     
         /**
    @@ -416,30 +480,19 @@ private function encrypt(
          *
          * @psalm-suppress TypeDoesNotContainType
          */
    -    private function decrypt(
    +    public function decrypt(
             string $data
         ) {
    -        // @codeCoverageIgnoreStart
    -        // @phpstan-ignore-next-line
    -        if (! self::USE_CRYPTO) {
    -            $returns = [];
    -            $decoded = base64_decode($data, true);
    -
    -            if (is_string($decoded)) {
    -                $decoded = json_decode($decoded, true, 512, JSON_THROW_ON_ERROR);
    -            }
    -
    -            if (is_string($decoded)) {
    -                $decoded = unserialize($decoded);
    -            }
    +        if (! $this->encrypt) {
    +            $decoded = rawurldecode($data);
    +            $decoded = json_decode($decoded, true);
     
                 if (is_array($decoded)) {
    -                $returns = $decoded;
    +                return $decoded;
                 }
     
    -            return $returns;
    +            return [];
             }
    -        // @codeCoverageIgnoreEnd
     
             [$data] = Toolkit::filter([$data])->string()->trim();
     
    @@ -453,14 +506,11 @@ private function decrypt(
                 throw \Auth0\SDK\Exception\ConfigurationException::requiresCookieSecret();
             }
     
    -        $data = json_decode((string) $data, true);
    +        $decoded = rawurldecode((string) $data);
    +        $stripped = stripslashes($decoded);
    +        $data = json_decode($stripped, true, 512);
     
    -        if (! is_string($data)) {
    -            return null;
    -        }
    -
    -        /** @var array{iv?: int|string|null, tag?: int|string|null, data: string} */
    -        $data = unserialize($data);
    +        /** @var array{iv?: int|string|null, tag?: int|string|null, data: string} $data */
     
             if (! isset($data['iv']) || ! isset($data['tag']) || ! is_string($data['iv']) || ! is_string($data['tag'])) {
                 return null;
    @@ -469,17 +519,17 @@ private function decrypt(
             $iv = base64_decode($data['iv'], true);
             $tag = base64_decode($data['tag'], true);
     
    -        if ($iv === false || $tag === false) {
    +        if (! is_string($iv) || ! is_string($tag)) {
                 return null;
             }
     
             $data = openssl_decrypt($data['data'], self::VAL_CRYPTO_ALGO, $secret, 0, $iv, $tag);
     
    -        if ($data === false) {
    +        if (! is_string($data)) {
                 return null;
             }
     
    -        $data = unserialize($data);
    +        $data = json_decode($data, true);
     
             /** @var array<mixed> $data */
             return $data;
    
  • tests/Unit/Store/CookieStoreTest.php+45 19 modified
    @@ -27,10 +27,6 @@
         $_COOKIE = [];
     });
     
    -it('hashes the namespace', function(): void {
    -    $this->assertNotEquals($this->namespace, $this->store->getNamespace());
    -});
    -
     it('calculates a chunking threshold', function(): void {
         expect($this->store->getThreshold())->toBeGreaterThan(0);
     });
    @@ -46,7 +42,8 @@
     it('populates state from $_COOKIE correctly', function(array $state): void {
         $cookieNamespace = $this->store->getNamespace() . '_0';
     
    -    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, serialize([$this->exampleKey => $state]));
    +    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, [$this->exampleKey => $state]);
    +
         $_COOKIE[$cookieNamespace] = $encrypted;
     
         $this->store->getState();
    @@ -57,7 +54,7 @@
     ]]);
     
     it('populates state from a chunked $_COOKIE correctly', function(array $state): void {
    -    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, serialize([$this->exampleKey => $state]));
    +    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, [$this->exampleKey => $state]);
         $chunks = str_split($encrypted, 32);
     
         foreach($chunks as $index => $chunk) {
    @@ -86,7 +83,7 @@
     it('does not populate state from an unencrypted $_COOKIE', function(array $state): void {
         $cookieNamespace = $this->store->getNamespace() . '_0';
     
    -    $_COOKIE[$cookieNamespace] = json_encode(serialize([$this->exampleKey => $state]));
    +    $_COOKIE[$cookieNamespace] = json_encode([$this->exampleKey => $state]);
     
         $this->store->getState();
     
    @@ -118,14 +115,15 @@
     test('delete() updates state and $_COOKIE', function(array $state): void {
         $cookieNamespace = $this->store->getNamespace() . '_0';
     
    -    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, serialize([$this->exampleKey => $state]));
    +    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, [$this->exampleKey => $state]);
         $_COOKIE[$cookieNamespace] = $encrypted;
     
         $this->store->getState();
     
         $previousCookieState = $_COOKIE[$cookieNamespace];
     
    -    $this->store->setState();
    +    // Force the state change. As we didn't use the class methods to mutate the session state, it won't be flagged as dirty internally.
    +    $this->store->setState(true);
     
         $this->assertNotEquals($_COOKIE[$cookieNamespace], $previousCookieState);
     
    @@ -140,7 +138,7 @@
     test('purge() clears state and $_COOKIE', function(array $state): void {
         $cookieNamespace = $this->store->getNamespace() . '_0';
     
    -    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, serialize([$this->exampleKey => $state]));
    +    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, [$this->exampleKey => $state]);
         $_COOKIE[$cookieNamespace] = $encrypted;
     
         $this->store->getState();
    @@ -176,7 +174,7 @@
         $this->store = new CookieStore($this->configuration, $this->namespace);
     
         $cookieNamespace = $this->store->getNamespace() . '_0';
    -    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, serialize([$this->exampleKey => $state]));
    +    $encrypted = MockCrypto::cookieCompatibleEncrypt($this->cookieSecret, [$this->exampleKey => $state]);
         $_COOKIE[$cookieNamespace] = $encrypted;
     
         $this->store->getState();
    @@ -193,16 +191,16 @@
     
     test('decrypt() returns null if a malformed cryptographic manifest is encoded', function(): void {
         $cookieNamespace = $this->store->getNamespace() . '_0';
    -    $_COOKIE[$cookieNamespace] = json_encode(serialize([
    +    $_COOKIE[$cookieNamespace] = json_encode([
             'tag' => uniqid()
    -    ]));
    +    ]);
     
         expect($this->store->getState())->toBeEmpty();
     
    -    $_COOKIE[$cookieNamespace] = json_encode(serialize([
    +    $_COOKIE[$cookieNamespace] = json_encode([
             'iv' => 'hi 👋 malformed cryptographic manifest here',
             'tag' => (string) uniqid()
    -    ]));
    +    ]);
     
         expect($this->store->getState())->toBeEmpty();
     });
    @@ -212,29 +210,57 @@
     
         $ivLength = openssl_cipher_iv_length(CookieStore::VAL_CRYPTO_ALGO);
         $iv = openssl_random_pseudo_bytes($ivLength);
    -    $payload = json_encode(serialize([
    +    $payload = json_encode([
             'tag' => base64_encode((string) uniqid()),
             'iv' => base64_encode($iv),
             'data' => 'not encrypted :eyes:'
    -    ]), JSON_THROW_ON_ERROR);
    +    ], JSON_THROW_ON_ERROR);
     
         $_COOKIE[$cookieNamespace] = $payload;
     
         expect($this->store->getState())->toBeEmpty();
     });
     
    -test('Configured SameSite() is reflected', function(): void {
    +test('configured SameSite() is reflected', function(): void {
         $this->configuration->setCookieSameSite('strict');
     
         $options = $this->store->getCookieOptions();
     
         expect($options['samesite'])->toEqual('strict');
     });
     
    -test('Unsupported configured SameSite() is overwritten by default of `lax`', function(): void {
    +test('unsupported configured SameSite() is overwritten by default of `lax`', function(): void {
         $this->configuration->setCookieSameSite('testing');
     
         $options = $this->store->getCookieOptions();
     
         expect($options['samesite'])->toEqual('Lax');
     });
    +
    +test('toggling encryption works', function(array $state): void {
    +    expect($this->store->getEncrypted())->toEqual(true);
    +
    +    $this->store->setEncrypted(false);
    +    expect($this->store->getEncrypted())->toEqual(false);
    +
    +    $encrypted = $this->store->encrypt($state);
    +    expect($encrypted)->toEqual(rawurlencode(json_encode($state)));
    +    expect($this->store->decrypt($encrypted))->toEqual($state);
    +    expect($this->store->decrypt(rawurlencode(json_encode('test'))))->toEqual([]);
    +})->with(['mocked state' => [
    +    fn() => MockDataset::state()
    +]]);
    +
    +test('encrypt() returns nothing with invalid crypto properties', function(): void {
    +    $state = MockDataset::state();
    +
    +    expect($this->store->encrypt($state, ['ivLen' => false]))->toEqual('');
    +    expect($this->store->encrypt($state, ['iv' => false]))->toEqual('');
    +    expect($this->store->encrypt($state, ['tag' => false]))->toEqual('');
    +    expect($this->store->encrypt($state, ['encrypted' => false]))->toEqual('');
    +    expect($this->store->encrypt($state, ['encoded1' => false]))->toEqual('');
    +    expect($this->store->encrypt($state, ['encoded2' => false]))->toEqual('');
    +
    +    $this->store->setEncrypted(false);
    +    expect($this->store->encrypt($state, ['encoded1' => false]))->toEqual('');
    +});
    
  • tests/Utilities/MockCrypto.php+13 8 modified
    @@ -16,21 +16,26 @@ class MockCrypto
          */
         public static function cookieCompatibleEncrypt(
             string $secret,
    -        string $data
    +        $data,
    +        ?string $overrideIv = null,
    +        ?string $overrideTag = null
         ): string {
             $ivLength = openssl_cipher_iv_length(CookieStore::VAL_CRYPTO_ALGO);
    -        $iv = openssl_random_pseudo_bytes($ivLength);
    -        $encrypted = openssl_encrypt($data, CookieStore::VAL_CRYPTO_ALGO, $secret, 0, $iv, $tag);
    -        $encrypted = json_encode(serialize([
    -            'tag' => base64_encode($tag),
    +
    +        $iv = $overrideIv ?? openssl_random_pseudo_bytes($ivLength);
    +
    +        $encrypted = openssl_encrypt(json_encode($data), CookieStore::VAL_CRYPTO_ALGO, $secret, 0, $iv, $tag);
    +
    +        $data = json_encode([
    +            'tag' => base64_encode($overrideTag ?? $tag),
                 'iv' => base64_encode($iv),
                 'data' => $encrypted
    -        ]), JSON_THROW_ON_ERROR);
    +        ]);
     
    -        if ($encrypted === false) {
    +        if (! is_string($data)) {
                 return '';
             }
     
    -        return $encrypted;
    +        return rawurlencode($data);
         }
     }
    
  • tests/Utilities/MockDataset.php+1 11 modified
    @@ -18,7 +18,7 @@ public static function state(
             int $depth = 0
         ): array {
             $response = [];
    -        $types = ['string', 'integer', 'float', 'boolean', 'array', 'object', 'null'];
    +        $types = ['string', 'integer', 'float', 'boolean', 'array', 'null'];
             $childCount = random_int(count($types), count($types) * 2);
     
             for ($k = 0; $k < $childCount; $k++) {
    @@ -55,16 +55,6 @@ public static function state(
                     continue;
                 }
     
    -            if ($type === 'object') {
    -                if ($depth >= 1) {
    -                    $response[$name] = (object) [];
    -                    continue;
    -                }
    -
    -                $response[$name] = (object) self::state($depth + 1);
    -                continue;
    -            }
    -
                 if ($type === null) {
                     $response[$name] = null;
                     continue;
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

7

News mentions

0

No linked articles in our index yet.