From b111e63bd3b86ca9747b1bbb3f7eae7b9c01a82e Mon Sep 17 00:00:00 2001 From: Masahiro Matsuya Date: Wed, 18 Feb 2026 13:54:42 +0900 Subject: [PATCH] Backport commit be353d7 Add limit of 20 continuation octets per OID arc to prevent a potential memory exhaustion from excessive continuation bytes input. --- pyasn1/codec/ber/decoder.py | 11 ++++++ tests/codec/ber/test_decoder.py | 66 +++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/pyasn1/codec/ber/decoder.py b/pyasn1/codec/ber/decoder.py index ee3064f..69c9ecd 100644 --- a/pyasn1/codec/ber/decoder.py +++ b/pyasn1/codec/ber/decoder.py @@ -14,6 +14,10 @@ __all__ = ['decode'] noValue = base.noValue +# Maximum number of continuation octets (high-bit set) allowed per OID arc. +# 20 octets allows up to 140-bit integers, supporting UUID-based OIDs +MAX_OID_ARC_CONTINUATION_OCTETS = 20 + class AbstractDecoder(object): protoComponent = None @@ -284,7 +288,14 @@ class ObjectIdentifierDecoder(AbstractSimpleDecoder): # Construct subid from a number of octets nextSubId = subId subId = 0 + continuationOctetCount = 0 while nextSubId >= 128: + continuationOctetCount += 1 + if continuationOctetCount > MAX_OID_ARC_CONTINUATION_OCTETS: + raise error.PyAsn1Error( + 'OID arc exceeds maximum continuation octets limit (%d) ' + 'at position %d' % (MAX_OID_ARC_CONTINUATION_OCTETS, index) + ) subId = (subId << 7) + (nextSubId & 0x7F) if index >= substrateLen: raise error.SubstrateUnderrunError( diff --git a/tests/codec/ber/test_decoder.py b/tests/codec/ber/test_decoder.py index 5ec3a5f..ba7dfec 100644 --- a/tests/codec/ber/test_decoder.py +++ b/tests/codec/ber/test_decoder.py @@ -403,6 +403,72 @@ class ObjectIdentifierDecoderTestCase(BaseTestCase): 0xB8, 0xCB, 0xE2, 0xB6, 0x47)) ) == ((2, 999, 18446744073709551535184467440737095), null) + def testExcessiveContinuationOctets(self): + """Test that OID arcs with excessive continuation octets are rejected.""" + # Create a payload with 25 continuation octets (exceeds 20 limit) + # 0x81 bytes are continuation octets, 0x01 terminates + malicious_payload = bytes([0x06, 26]) + bytes([0x81] * 25) + bytes([0x01]) + try: + decoder.decode(malicious_payload) + except PyAsn1Error: + pass + else: + assert 0, 'Excessive continuation octets tolerated' + + def testMaxAllowedContinuationOctets(self): + """Test that OID arcs at the maximum continuation octets limit work.""" + # Create a payload with exactly 20 continuation octets (at limit) + # This should succeed + payload = bytes([0x06, 21]) + bytes([0x81] * 20) + bytes([0x01]) + try: + decoder.decode(payload) + except PyAsn1Error: + assert 0, 'Valid OID with 20 continuation octets rejected' + + def testOneOverContinuationLimit(self): + """Test boundary: 21 continuation octets (one over limit) is rejected.""" + payload = bytes([0x06, 22]) + bytes([0x81] * 21) + bytes([0x01]) + try: + decoder.decode(payload) + except PyAsn1Error: + pass + else: + assert 0, '21 continuation octets tolerated (should be rejected)' + + def testExcessiveContinuationInSecondArc(self): + """Test that limit applies to subsequent arcs, not just the first.""" + # First arc: valid simple byte (0x55 = 85, decodes to arc 2.5) + # Second arc: excessive continuation octets + payload = bytes([0x06, 27]) + bytes([0x55]) + bytes([0x81] * 25) + bytes([0x01]) + try: + decoder.decode(payload) + except PyAsn1Error: + pass + else: + assert 0, 'Excessive continuation in second arc tolerated' + + def testMultipleArcsAtLimit(self): + """Test multiple arcs each at the continuation limit work correctly.""" + # Two arcs, each with 20 continuation octets (both at limit) + arc1 = bytes([0x81] * 20) + bytes([0x01]) # 21 bytes + arc2 = bytes([0x81] * 20) + bytes([0x01]) # 21 bytes + payload = bytes([0x06, 42]) + arc1 + arc2 + try: + decoder.decode(payload) + except PyAsn1Error: + assert 0, 'Multiple valid arcs at limit rejected' + + def testExcessiveContinuationWithMaxBytes(self): + """Test with 0xFF continuation bytes (maximum value, not just 0x81).""" + # 0xFF bytes are also continuation octets (high bit set) + malicious_payload = bytes([0x06, 26]) + bytes([0xFF] * 25) + bytes([0x01]) + try: + decoder.decode(malicious_payload) + except PyAsn1Error: + pass + else: + assert 0, 'Excessive 0xFF continuation octets tolerated' + class RealDecoderTestCase(BaseTestCase): def testChar(self): -- 2.52.0