diff --git a/src/Library/Encryption/Serializer/CompactSerializer.php b/src/Library/Encryption/Serializer/CompactSerializer.php index 026b2f916..e42b6c553 100644 --- a/src/Library/Encryption/Serializer/CompactSerializer.php +++ b/src/Library/Encryption/Serializer/CompactSerializer.php @@ -54,10 +54,16 @@ public function serialize(JWE $jwe, ?int $recipientIndex = null): string ); } + /** + * The split is bounded to six segments: a valid compact JWE has exactly five, so a sixth one is enough to + * detect and reject any longer input. Without that bound, a delimiter-heavy string would be expanded into one + * array entry per delimiter before the segment count is checked, which costs about twenty-five times the size + * of the input in memory. + */ #[Override] public function unserialize(string $input): JWE { - $parts = explode('.', $input); + $parts = explode('.', $input, 6); if (count($parts) !== 5) { throw new InvalidArgumentException('Unsupported input'); } diff --git a/src/Library/Signature/Serializer/CompactSerializer.php b/src/Library/Signature/Serializer/CompactSerializer.php index 4d1a46261..c7af874a9 100644 --- a/src/Library/Signature/Serializer/CompactSerializer.php +++ b/src/Library/Signature/Serializer/CompactSerializer.php @@ -58,10 +58,16 @@ public function serialize(JWS $jws, ?int $signatureIndex = null): string ); } + /** + * The split is bounded to four segments: a valid compact JWS has exactly three, so a fourth one is enough to + * detect and reject any longer input. Without that bound, a delimiter-heavy string would be expanded into one + * array entry per delimiter before the segment count is checked, which costs about twenty-five times the size + * of the input in memory. + */ #[Override] public function unserialize(string $input): JWS { - $parts = explode('.', $input); + $parts = explode('.', $input, 4); if (count($parts) !== 3) { throw new InvalidArgumentException('Unsupported input'); } diff --git a/tests/Component/Encryption/CompactSerializerTest.php b/tests/Component/Encryption/CompactSerializerTest.php new file mode 100644 index 000000000..e1461b5f0 --- /dev/null +++ b/tests/Component/Encryption/CompactSerializerTest.php @@ -0,0 +1,52 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported input'); + + (new CompactSerializer())->unserialize('eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0.....'); + } + + /** + * A delimiter-heavy token must be rejected without expanding it into one array entry per delimiter. The + * threshold is deliberately generous: the unbounded split of this input allocates roughly twenty-five times its + * size, while the bounded one stays proportional to it. + */ + #[Test] + public function aDelimiterHeavyTokenIsRejectedWithoutExhaustingMemory(): void + { + $token = str_repeat('.', 2_000_000); + $serializer = new CompactSerializer(); + memory_reset_peak_usage(); + $before = memory_get_usage(); + + try { + $serializer->unserialize($token); + static::fail('The token should have been rejected.'); + } catch (InvalidArgumentException $e) { + static::assertSame('Unsupported input', $e->getMessage()); + } + + static::assertLessThan(8_000_000, memory_get_peak_usage() - $before); + } +} diff --git a/tests/Component/Signature/CompactSerializerTest.php b/tests/Component/Signature/CompactSerializerTest.php new file mode 100644 index 000000000..93221ee02 --- /dev/null +++ b/tests/Component/Signature/CompactSerializerTest.php @@ -0,0 +1,52 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Unsupported input'); + + (new CompactSerializer())->unserialize('eyJhbGciOiJub25lIn0...'); + } + + /** + * A delimiter-heavy token must be rejected without expanding it into one array entry per delimiter. The + * threshold is deliberately generous: the unbounded split of this input allocates roughly twenty-five times its + * size, while the bounded one stays proportional to it. + */ + #[Test] + public function aDelimiterHeavyTokenIsRejectedWithoutExhaustingMemory(): void + { + $token = str_repeat('.', 2_000_000); + $serializer = new CompactSerializer(); + memory_reset_peak_usage(); + $before = memory_get_usage(); + + try { + $serializer->unserialize($token); + static::fail('The token should have been rejected.'); + } catch (InvalidArgumentException $e) { + static::assertSame('Unsupported input', $e->getMessage()); + } + + static::assertLessThan(8_000_000, memory_get_peak_usage() - $before); + } +}