From 340e4a62ec036e19f709d33017bcb5ca8deaa352 Mon Sep 17 00:00:00 2001 From: Oinkling Date: Thu, 8 Aug 2024 12:30:15 +0200 Subject: [PATCH 1/3] fix: Throw exception on invalid base64 Fixed JWT::urlsafeB64Decode not actually throwing an InvalidArgumentException on invalid base64 characters, despite doc comment saying that it does. --- src/JWT.php | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/src/JWT.php b/src/JWT.php index e9d75639..9189d694 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -109,14 +109,22 @@ public static function decode( throw new UnexpectedValueException('Wrong number of segments'); } list($headb64, $bodyb64, $cryptob64) = $tks; - $headerRaw = static::urlsafeB64Decode($headb64); + try { + $headerRaw = static::urlsafeB64Decode($headb64); + } catch (InvalidArgumentException $e) { + throw new UnexpectedValueException('Unable to decode header'); + } if (null === ($header = static::jsonDecode($headerRaw))) { throw new UnexpectedValueException('Invalid header encoding'); } if ($headers !== null) { $headers = $header; } - $payloadRaw = static::urlsafeB64Decode($bodyb64); + try { + $payloadRaw = static::urlsafeB64Decode($bodyb64); + } catch (InvalidArgumentException $e) { + throw new UnexpectedValueException('Unable to decode payload'); + } if (null === ($payload = static::jsonDecode($payloadRaw))) { throw new UnexpectedValueException('Invalid claims encoding'); } @@ -127,7 +135,11 @@ public static function decode( if (!$payload instanceof stdClass) { throw new UnexpectedValueException('Payload must be a JSON object'); } - $sig = static::urlsafeB64Decode($cryptob64); + try { + $sig = static::urlsafeB64Decode($cryptob64); + } catch (InvalidArgumentException $e) { + throw new UnexpectedValueException('Unable to decode signature'); + } if (empty($header->alg)) { throw new UnexpectedValueException('Empty algorithm'); } @@ -415,7 +427,11 @@ public static function jsonEncode(array $input): string */ public static function urlsafeB64Decode(string $input): string { - return \base64_decode(self::convertBase64UrlToBase64($input)); + $result = \base64_decode(self::convertBase64UrlToBase64($input), true); + if ($result === false) { + throw new InvalidArgumentException('Input is not valid Base64URL'); + } + return $result; } /** From 86ebae1b04b3cdc78f38ce4ada6bd3c8f5f9e847 Mon Sep 17 00:00:00 2001 From: Oinkling Date: Mon, 28 Apr 2025 13:04:50 +0200 Subject: [PATCH 2/3] urlsafeB64Decode will fail on any invalid input Last change still allowed input that was invalid base64URL but valid base64 --- src/JWT.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/JWT.php b/src/JWT.php index 9189d694..2a3f6aaa 100644 --- a/src/JWT.php +++ b/src/JWT.php @@ -423,10 +423,13 @@ public static function jsonEncode(array $input): string * * @return string A decoded string * - * @throws InvalidArgumentException invalid base64 characters + * @throws InvalidArgumentException invalid base64URL characters */ public static function urlsafeB64Decode(string $input): string { + if (strpbrk($input, '+/=') !== false) { + throw new InvalidArgumentException('Input is not valid Base64URL'); + } $result = \base64_decode(self::convertBase64UrlToBase64($input), true); if ($result === false) { throw new InvalidArgumentException('Input is not valid Base64URL'); From 7908d58e9c4c2fdf3a0037c552d5bdb8d04cdb45 Mon Sep 17 00:00:00 2001 From: Oinkling Date: Mon, 28 Apr 2025 13:07:00 +0200 Subject: [PATCH 3/3] Tests for urlsafeB64Decode --- tests/JWTTest.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/JWTTest.php b/tests/JWTTest.php index d09d43e3..ac1e1ff5 100644 --- a/tests/JWTTest.php +++ b/tests/JWTTest.php @@ -38,6 +38,25 @@ public function testMalformedJsonThrowsException() JWT::jsonDecode('this is not valid JSON string'); } + public function testBase64UrlDecode() + { + $decoded = JWT::urlsafeB64Decode('VGVzdCB3aXRoIGEgbWludXM-'); + $expected = 'Test with a minus>'; + $this->assertSame($expected, $decoded); + } + + public function testMalformedBase64Url() + { + $this->expectException(InvalidArgumentException::class); + JWT::urlsafeB64Decode('VGVzdCB3aXR&oIGEgbWludXM-'); + } + + public function testMalformedBase64UrlButValidBase64() + { + $this->expectException(InvalidArgumentException::class); + JWT::urlsafeB64Decode('VGVzdCB3aXRoIGEgc2xhc2g/'); + } + public function testExpiredToken() { $this->expectException(ExpiredException::class);