File CVE-2026-30922.patch of Package python-pyasn1.43237
Index: pyasn1-0.4.2/pyasn1/codec/ber/decoder.py
===================================================================
--- pyasn1-0.4.2.orig/pyasn1/codec/ber/decoder.py
+++ pyasn1-0.4.2/pyasn1/codec/ber/decoder.py
@@ -39,6 +39,7 @@ class AbstractDecoder(object):
# 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
+MAX_NESTING_DEPTH = 90
class AbstractSimpleDecoder(AbstractDecoder):
@@ -1082,6 +1083,15 @@ class Decoder(object):
if logger:
logger('decoder called at scope %s with state %d, working with up to %d octets of substrate: %s' % (debug.scope, state, len(substrate), debug.hexdump(substrate)))
+ _nestingLevel = options.get('_nestingLevel', 0)
+
+ if _nestingLevel > MAX_NESTING_DEPTH:
+ raise error.PyAsn1Error(
+ 'ASN.1 structure nesting depth exceeds limit (%d)' % MAX_NESTING_DEPTH
+ )
+
+ options['_nestingLevel'] = _nestingLevel + 1
+
allowEoo = options.pop('allowEoo', False)
# Look for end-of-octets sentinel
Index: pyasn1-0.4.2/tests/codec/ber/test_decoder.py
===================================================================
--- pyasn1-0.4.2.orig/tests/codec/ber/test_decoder.py
+++ pyasn1-0.4.2/tests/codec/ber/test_decoder.py
@@ -26,6 +26,22 @@ from pyasn1 import error
from pyasn1.error import PyAsn1Error
+def bit_length(n):
+ s = bin(n)
+ s = s.lstrip('-0b')
+ return len(s)
+
+def to_bytes(n, length=1, byteorder='big', signed=False):
+ if byteorder == 'little':
+ order = range(length)
+ elif byteorder == 'big':
+ order = reversed(range(length))
+ else:
+ raise ValueError("byteorder must be either 'little' or 'big'")
+
+ return bytes((n >> i*8) & 0xff for i in order)
+
+
class LargeTagDecoderTestCase(BaseTestCase):
def testLargeTag(self):
assert decoder.decode(ints2octs((127, 141, 245, 182, 253, 47, 3, 2, 1, 1))) == (1, null)
@@ -1495,6 +1511,121 @@ class NonStringDecoderTestCase(BaseTestC
s, _ = decoder.decode(univ.Any(self.substrate), asn1Spec=self.s)
assert self.s == s
+class NestingDepthLimitTestCase(BaseTestCase):
+ """Test protection against deeply nested ASN.1 structures (CVE prevention)."""
+
+ def testIndefLenSequenceNesting(self):
+ """Deeply nested indefinite-length SEQUENCEs must raise PyAsn1Error."""
+ # Each \x30\x80 opens a new indefinite-length SEQUENCE
+ payload = b'\x30\x80' * 200
+ try:
+ decoder.decode(payload)
+ except error.PyAsn1Error:
+ pass
+ else:
+ assert False, 'Deeply nested indef-length SEQUENCEs not rejected'
+
+ def testIndefLenSetNesting(self):
+ """Deeply nested indefinite-length SETs must raise PyAsn1Error."""
+ # Each \x31\x80 opens a new indefinite-length SET
+ payload = b'\x31\x80' * 200
+ try:
+ decoder.decode(payload)
+ except error.PyAsn1Error:
+ pass
+ else:
+ assert False, 'Deeply nested indef-length SETs not rejected'
+
+ def testDefiniteLenNesting(self):
+ """Deeply nested definite-length SEQUENCEs must raise PyAsn1Error."""
+ inner = b'\x05\x00' # NULL
+ for _ in range(200):
+ length = len(inner)
+ if length < 128:
+ inner = b'\x30' + bytes([length]) + inner
+ else:
+ length_bytes = to_bytes(length,
+ (bit_length(length) + 7) // 8, 'big')
+ inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
+ length_bytes + inner
+ try:
+ decoder.decode(inner)
+ except error.PyAsn1Error:
+ pass
+ else:
+ assert False, 'Deeply nested definite-length SEQUENCEs not rejected'
+
+ def testNestingUnderLimitWorks(self):
+ """Nesting within the limit must decode successfully."""
+ inner = b'\x05\x00' # NULL
+ for _ in range(50):
+ length = len(inner)
+ if length < 128:
+ inner = b'\x30' + bytes([length]) + inner
+ else:
+ length_bytes = to_bytes(length,
+ (bit_length(length) + 7) // 8, 'big')
+ inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
+ length_bytes + inner
+ asn1Object, _ = decoder.decode(inner)
+ assert asn1Object is not None, 'Valid nested structure rejected'
+
+ def testSiblingsDontIncreaseDepth(self):
+ """Sibling elements at the same level must not inflate depth count."""
+ # SEQUENCE containing 200 INTEGER siblings - should decode fine
+ components = b'\x02\x01\x01' * 200 # 200 x INTEGER(1)
+ length = len(components)
+ length_bytes = to_bytes(length,
+ (bit_length(length) + 7) // 8, 'big')
+ payload = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
+ length_bytes + components
+ asn1Object, _ = decoder.decode(payload)
+ assert asn1Object is not None, 'Siblings incorrectly rejected'
+
+ def testErrorMessageContainsLimit(self):
+ """Error message must indicate the nesting depth limit."""
+ payload = b'\x30\x80' * 200
+ try:
+ decoder.decode(payload)
+ except error.PyAsn1Error as exc:
+ assert 'nesting depth' in str(exc).lower(), \
+ 'Error message missing depth info: %s' % exc
+ else:
+ assert False, 'Expected PyAsn1Error'
+
+ def testNoRecursionError(self):
+ """Must raise PyAsn1Error, not RecursionError."""
+ payload = b'\x30\x80' * 50000
+ try:
+ decoder.decode(payload)
+ except error.PyAsn1Error:
+ pass
+ except RecursionError:
+ assert False, 'Got RecursionError instead of PyAsn1Error'
+
+ def testMixedNesting(self):
+ """Mixed SEQUENCE and SET nesting must be caught."""
+ # Alternate SEQUENCE (0x30) and SET (0x31) with indef length
+ payload = b''
+ for i in range(200):
+ payload += b'\x30\x80' if i % 2 == 0 else b'\x31\x80'
+ try:
+ decoder.decode(payload)
+ except error.PyAsn1Error:
+ pass
+ else:
+ assert False, 'Mixed nesting not rejected'
+
+ def testWithSchema(self):
+ """Deeply nested structures must be caught even with schema."""
+ payload = b'\x30\x80' * 200
+ try:
+ decoder.decode(payload, asn1Spec=univ.Sequence())
+ except error.PyAsn1Error:
+ pass
+ else:
+ assert False, 'Deeply nested with schema not rejected'
+
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
Index: pyasn1-0.4.2/tests/codec/cer/test_decoder.py
===================================================================
--- pyasn1-0.4.2.orig/tests/codec/cer/test_decoder.py
+++ pyasn1-0.4.2/tests/codec/cer/test_decoder.py
@@ -65,6 +65,30 @@ class OctetStringDecoderTestCase(BaseTes
# TODO: test failures on short chunked and long unchunked substrate samples
+class NestingDepthLimitTestCase(BaseTestCase):
+ """Test CER decoder protection against deeply nested structures."""
+
+ def testIndefLenNesting(self):
+ """Deeply nested indefinite-length SEQUENCEs must raise PyAsn1Error."""
+ payload = b'\x30\x80' * 200
+ try:
+ decoder.decode(payload)
+ except PyAsn1Error:
+ pass
+ else:
+ assert False, 'Deeply nested indef-length SEQUENCEs not rejected'
+
+ def testNoRecursionError(self):
+ """Must raise PyAsn1Error, not RecursionError."""
+ payload = b'\x30\x80' * 50000
+ try:
+ decoder.decode(payload)
+ except PyAsn1Error:
+ pass
+ except RecursionError:
+ assert False, 'Got RecursionError instead of PyAsn1Error'
+
+
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
if __name__ == '__main__':
Index: pyasn1-0.4.2/tests/codec/der/test_decoder.py
===================================================================
--- pyasn1-0.4.2.orig/tests/codec/der/test_decoder.py
+++ pyasn1-0.4.2/tests/codec/der/test_decoder.py
@@ -19,6 +19,22 @@ from pyasn1.compat.octets import ints2oc
from pyasn1.error import PyAsn1Error
+def bit_length(n):
+ s = bin(n)
+ s = s.lstrip('-0b')
+ return len(s)
+
+def to_bytes(n, length=1, byteorder='big', signed=False):
+ if byteorder == 'little':
+ order = range(length)
+ elif byteorder == 'big':
+ order = reversed(range(length))
+ else:
+ raise ValueError("byteorder must be either 'little' or 'big'")
+
+ return bytes((n >> i*8) & 0xff for i in order)
+
+
class BitStringDecoderTestCase(BaseTestCase):
def testShortMode(self):
assert decoder.decode(
@@ -73,6 +89,48 @@ class OctetStringDecoderTestCase(BaseTes
assert 0, 'chunked encoding tolerated'
+class NestingDepthLimitTestCase(BaseTestCase):
+ """Test DER decoder protection against deeply nested structures."""
+
+ def testDefiniteLenNesting(self):
+ """Deeply nested definite-length SEQUENCEs must raise PyAsn1Error."""
+ inner = b'\x05\x00' # NULL
+ for _ in range(200):
+ length = len(inner)
+ if length < 128:
+ inner = b'\x30' + bytes([length]) + inner
+ else:
+ length_bytes = to_bytes(length,
+ (bit_length(length) + 7) // 8, 'big')
+ inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
+ length_bytes + inner
+ try:
+ decoder.decode(inner)
+ except PyAsn1Error:
+ pass
+ else:
+ assert False, 'Deeply nested definite-length SEQUENCEs not rejected'
+
+ def testNoRecursionError(self):
+ """Must raise PyAsn1Error, not RecursionError."""
+ inner = b'\x05\x00'
+ for _ in range(200):
+ length = len(inner)
+ if length < 128:
+ inner = b'\x30' + bytes([length]) + inner
+ else:
+ length_bytes = to_bytes(length,
+ (bit_length(length) + 7) // 8, 'big')
+ inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
+ length_bytes + inner
+ try:
+ decoder.decode(inner)
+ except PyAsn1Error:
+ pass
+ except RecursionError:
+ assert False, 'Got RecursionError instead of PyAsn1Error'
+
+
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
if __name__ == '__main__':