From cd597c9463cf8095b7e75fb233a0425eddd1f2e6 Mon Sep 17 00:00:00 2001 From: Chad Sikorra Date: Mon, 25 May 2026 22:42:41 -0400 Subject: [PATCH] Add an option for max PDU length. --- src/FreeDSx/Asn1/Encoder/BerEncoder.php | 14 ++++++++++ src/FreeDSx/Asn1/Encoder/EncoderInterface.php | 2 ++ src/FreeDSx/Asn1/Encoder/EncoderOptions.php | 4 +++ .../Asn1/Exception/PduLengthException.php | 20 +++++++++++++ tests/unit/Encoder/BerEncoderTest.php | 28 +++++++++++++++++++ 5 files changed, 68 insertions(+) create mode 100644 src/FreeDSx/Asn1/Exception/PduLengthException.php diff --git a/src/FreeDSx/Asn1/Encoder/BerEncoder.php b/src/FreeDSx/Asn1/Encoder/BerEncoder.php index 845bb97..27f9b03 100644 --- a/src/FreeDSx/Asn1/Encoder/BerEncoder.php +++ b/src/FreeDSx/Asn1/Encoder/BerEncoder.php @@ -15,6 +15,7 @@ use FreeDSx\Asn1\Exception\EncoderException; use FreeDSx\Asn1\Exception\InvalidArgumentException; use FreeDSx\Asn1\Exception\PartialPduException; +use FreeDSx\Asn1\Exception\PduLengthException; use FreeDSx\Asn1\Type\AbstractTimeType; use FreeDSx\Asn1\Type\AbstractType; use FreeDSx\Asn1\Type as EncodedType; @@ -176,6 +177,8 @@ public function __construct(protected EncoderOptions $options = new EncoderOptio * {@inheritdoc} * * @return AbstractType + * + * @throws PduLengthException */ public function decode( string $binary, @@ -364,6 +367,7 @@ protected function stopEncoding(): void * * @throws EncoderException * @throws PartialPduException + * @throws PduLengthException */ protected function decodeBytes(bool $isRoot = false, $tagType = null, $length = null, $isConstructed = null, $class = null): AbstractType { @@ -397,6 +401,16 @@ protected function decodeBytes(bool $isRoot = false, $tagType = null, $length = if ($length > 128) { $length = $this->decodeLongDefiniteLength($length); } + + # Reject an oversized root PDU on its declared length alone, before its body is buffered or decoded. + if ($isRoot && $this->options->maxLength > 0 && $length > $this->options->maxLength) { + throw new PduLengthException(sprintf( + 'The PDU length %d exceeds the maximum allowed length of %d.', + $length, + $this->options->maxLength, + )); + } + $tagType = ($class === AbstractType::TAG_CLASS_UNIVERSAL) ? $tagNumber : ($this->tmpTagMap[$class][$tagNumber] ?? null); if (($this->maxLen - $this->pos) < $length) { diff --git a/src/FreeDSx/Asn1/Encoder/EncoderInterface.php b/src/FreeDSx/Asn1/Encoder/EncoderInterface.php index 2b3e21d..09b9d91 100644 --- a/src/FreeDSx/Asn1/Encoder/EncoderInterface.php +++ b/src/FreeDSx/Asn1/Encoder/EncoderInterface.php @@ -12,6 +12,7 @@ use FreeDSx\Asn1\Exception\EncoderException; use FreeDSx\Asn1\Exception\PartialPduException; +use FreeDSx\Asn1\Exception\PduLengthException; use FreeDSx\Asn1\Type\AbstractType; use FreeDSx\Asn1\Type\IncompleteType; @@ -51,6 +52,7 @@ public function complete(IncompleteType $type, int $tagType, array $tagMap = []) * * @throws EncoderException * @throws PartialPduException + * @throws PduLengthException */ public function decode( string $binary, diff --git a/src/FreeDSx/Asn1/Encoder/EncoderOptions.php b/src/FreeDSx/Asn1/Encoder/EncoderOptions.php index fe72857..3a370c9 100644 --- a/src/FreeDSx/Asn1/Encoder/EncoderOptions.php +++ b/src/FreeDSx/Asn1/Encoder/EncoderOptions.php @@ -18,8 +18,12 @@ */ final readonly class EncoderOptions { + /** + * @param int $maxLength Reject a root PDU whose declared length exceeds this many bytes; 0 disables the limit. + */ public function __construct( public string $bitstringPadding = '0', + public int $maxLength = 0, ) { } } diff --git a/src/FreeDSx/Asn1/Exception/PduLengthException.php b/src/FreeDSx/Asn1/Exception/PduLengthException.php new file mode 100644 index 0000000..173a8a5 --- /dev/null +++ b/src/FreeDSx/Asn1/Exception/PduLengthException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace FreeDSx\Asn1\Exception; + +/** + * Thrown when a root PDU's declared length exceeds the configured maximum. + * + * @author Chad Sikorra + */ +class PduLengthException extends EncoderException +{ +} diff --git a/tests/unit/Encoder/BerEncoderTest.php b/tests/unit/Encoder/BerEncoderTest.php index 32bfacf..99588df 100644 --- a/tests/unit/Encoder/BerEncoderTest.php +++ b/tests/unit/Encoder/BerEncoderTest.php @@ -19,6 +19,7 @@ use FreeDSx\Asn1\Encoder\EncoderOptions; use FreeDSx\Asn1\Exception\EncoderException; use FreeDSx\Asn1\Exception\PartialPduException; +use FreeDSx\Asn1\Exception\PduLengthException; use FreeDSx\Asn1\Type\AbstractType; use FreeDSx\Asn1\Type\BitStringType; use FreeDSx\Asn1\Type\BmpStringType; @@ -86,6 +87,33 @@ public function test_it_should_accept_options_via_the_constructor(): void ); } + public function test_it_should_reject_a_root_pdu_whose_declared_length_exceeds_the_max_length(): void + { + $subject = new BerEncoder(new EncoderOptions(maxLength: 100)); + + $this->expectException(PduLengthException::class); + + $subject->decode(hex2bin('3084000000c8')); + } + + public function test_it_should_decode_a_root_pdu_within_the_max_length(): void + { + $subject = new BerEncoder(new EncoderOptions(maxLength: 1000)); + $encoded = $subject->encode(new OctetStringType('foo')); + + self::assertEquals( + new OctetStringType('foo'), + $subject->decode($encoded), + ); + } + + public function test_it_should_treat_an_oversized_declared_length_as_partial_when_no_max_length_is_set(): void + { + $this->expectException(PartialPduException::class); + + $this->subject->decode(hex2bin('3084000000c8')); + } + public function test_it_should_decode_long_definite_length_when_the_length_is_the_exact_size_of_the_payload(): void { $tagAndLength = hex2bin('3084000000350201');