Skip to content

Commit 958e422

Browse files
fix: validate numeric type of iat, nbf and exp claims in encode (#634)
* fix: validate numeric type of iat, nbf and exp claims in encode The decode() path already rejected non-numeric iat/nbf/exp values, but encode() accepted any type and silently produced an invalid token. Add the same is_numeric guards in encode() and cover them with unit tests. Signed-off-by: Guillaume Delré <delre.guillaume@gmail.com> * test: add regression test for numeric string timestamps in encode Covers the case where iat/nbf/exp are passed as numeric strings (e.g. (string) time()), which must remain accepted to avoid a breaking change. Signed-off-by: Guillaume Delré <delre.guillaume@gmail.com> --------- Signed-off-by: Guillaume Delré <delre.guillaume@gmail.com> Co-authored-by: Brent Shaffer <betterbrent@google.com>
1 parent bc9b2ad commit 958e422

2 files changed

Lines changed: 48 additions & 0 deletions

File tree

src/JWT.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,15 @@ public static function encode(
224224
if ($keyId !== null) {
225225
$header['kid'] = $keyId;
226226
}
227+
if (isset($payload['nbf']) && !\is_numeric($payload['nbf'])) {
228+
throw new UnexpectedValueException('Payload nbf must be a number');
229+
}
230+
if (isset($payload['iat']) && !\is_numeric($payload['iat'])) {
231+
throw new UnexpectedValueException('Payload iat must be a number');
232+
}
233+
if (isset($payload['exp']) && !\is_numeric($payload['exp'])) {
234+
throw new UnexpectedValueException('Payload exp must be a number');
235+
}
227236
$segments = [];
228237
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($header));
229238
$segments[] = static::urlsafeB64Encode((string) static::jsonEncode($payload));

tests/JWTTest.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,45 @@ public function testDecodeExpectsIntegerExp()
609609
JWT::decode($payload, $this->hmacKey);
610610
}
611611

612+
public function testEncodeThrowsWhenIatIsNotNumeric(): void
613+
{
614+
$this->expectException(UnexpectedValueException::class);
615+
$this->expectExceptionMessage('Payload iat must be a number');
616+
617+
JWT::encode(['iat' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
618+
}
619+
620+
public function testEncodeThrowsWhenNbfIsNotNumeric(): void
621+
{
622+
$this->expectException(UnexpectedValueException::class);
623+
$this->expectExceptionMessage('Payload nbf must be a number');
624+
625+
JWT::encode(['nbf' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
626+
}
627+
628+
public function testEncodeThrowsWhenExpIsNotNumeric(): void
629+
{
630+
$this->expectException(UnexpectedValueException::class);
631+
$this->expectExceptionMessage('Payload exp must be a number');
632+
633+
JWT::encode(['exp' => 'not-an-int'], $this->hmacKey->getKeyMaterial(), 'HS256');
634+
}
635+
636+
public function testEncodeAcceptsNumericStringTimestamps(): void
637+
{
638+
$payload = [
639+
'message' => 'abc',
640+
'iat' => (string) time(),
641+
'exp' => (string) (time() + 20),
642+
'nbf' => (string) (time() - 20),
643+
];
644+
645+
$encoded = JWT::encode($payload, $this->hmacKey->getKeyMaterial(), 'HS256');
646+
$decoded = JWT::decode($encoded, $this->hmacKey);
647+
648+
$this->assertSame('abc', $decoded->message);
649+
}
650+
612651
public function testRsaKeyLengthValidationThrowsException(): void
613652
{
614653
$this->expectException(DomainException::class);

0 commit comments

Comments
 (0)