CVE-2026-30922
Description
pyasn1 is a generic ASN.1 library for Python. Prior to 0.6.3, the pyasn1 library is vulnerable to a Denial of Service (DoS) attack caused by uncontrolled recursion when decoding ASN.1 data with deeply nested structures. An attacker can supply a crafted payload containing thousands of nested SEQUENCE (0x30) or SET (0x31) tags with "Indefinite Length" (0x80) markers. This forces the decoder to recursively call itself until the Python interpreter crashes with a RecursionError or consumes all available memory (OOM), crashing the host application. This is a distinct vulnerability from CVE-2026-23490 (which addressed integer overflows in OID decoding). The fix for CVE-2026-23490 (MAX_OID_ARC_CONTINUATION_OCTETS) does not mitigate this recursion issue. Version 0.6.3 fixes this specific issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
pyasn1PyPI | < 0.6.3 | 0.6.3 |
Affected products
1Patches
25a49bd1fe93bMerge commit from fork
4 files changed · +192 −0
pyasn1/codec/ber/decoder.py+10 −0 modified@@ -36,6 +36,7 @@ # 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 = 100 # Maximum number of bytes in a BER length field (8 bytes = up to 2^64-1) MAX_LENGTH_OCTETS = 8 @@ -1568,6 +1569,15 @@ def __call__(self, substrate, asn1Spec=None, decodeFun=None, substrateFun=None, **options): + _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) if LOG:
tests/codec/ber/test_decoder.py+116 −0 modified@@ -2172,6 +2172,122 @@ def testErrorMessageContainsLimit(self): assert False, 'Expected PyAsn1Error' +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 = length.to_bytes( + (length.bit_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 = length.to_bytes( + (length.bit_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 = length.to_bytes( + (length.bit_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' + + class NonStreamingCompatibilityTestCase(BaseTestCase): def setUp(self): from pyasn1 import debug
tests/codec/cer/test_decoder.py+24 −0 modified@@ -363,6 +363,30 @@ def testDecodeOpenTypesUnknownId(self): assert s[1][0] == univ.OctetString(hexValue='02010C') +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__':
tests/codec/der/test_decoder.py+42 −0 modified@@ -361,6 +361,48 @@ def testDecodeOpenTypesUnknownId(self): assert s[1][0] == univ.OctetString(hexValue='02010C') +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 = length.to_bytes( + (length.bit_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 = length.to_bytes( + (length.bit_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__':
25ad481c19fdMerge commit from fork
4 files changed · +192 −0
pyasn1/codec/ber/decoder.py+10 −0 modified@@ -36,6 +36,7 @@ # 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 = 100 # Maximum number of bytes in a BER length field (8 bytes = up to 2^64-1) MAX_LENGTH_OCTETS = 8 @@ -1568,6 +1569,15 @@ def __call__(self, substrate, asn1Spec=None, decodeFun=None, substrateFun=None, **options): + _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) if LOG:
tests/codec/ber/test_decoder.py+116 −0 modified@@ -2186,6 +2186,122 @@ def testErrorMessageContainsLimit(self): assert False, 'Expected PyAsn1Error' +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 = length.to_bytes( + (length.bit_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 = length.to_bytes( + (length.bit_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 = length.to_bytes( + (length.bit_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' + + class NonStreamingCompatibilityTestCase(BaseTestCase): def setUp(self): from pyasn1 import debug
tests/codec/cer/test_decoder.py+24 −0 modified@@ -363,6 +363,30 @@ def testDecodeOpenTypesUnknownId(self): assert s[1][0] == univ.OctetString(hexValue='02010C') +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__':
tests/codec/der/test_decoder.py+42 −0 modified@@ -361,6 +361,48 @@ def testDecodeOpenTypesUnknownId(self): assert s[1][0] == univ.OctetString(hexValue='02010C') +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 = length.to_bytes( + (length.bit_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 = length.to_bytes( + (length.bit_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__':
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/pyasn1/pyasn1/commit/25ad481c19fdb006e20485ef3fc2e5b3eff30ef0nvdPatchWEB
- github.com/pyasn1/pyasn1/security/advisories/GHSA-jr27-m4p2-rc6rnvdExploitVendor AdvisoryWEB
- github.com/advisories/GHSA-jr27-m4p2-rc6rghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-30922ghsaADVISORY
- www.openwall.com/lists/oss-security/2026/03/20/4nvdWEB
- github.com/pyasn1/pyasn1/commit/5a49bd1fe93b5b866a1210f6bf0a3924f21572c8ghsaWEB
- github.com/pyasn1/pyasn1/releases/tag/v0.6.3ghsaWEB
- lists.debian.org/debian-lts-announce/2026/05/msg00001.htmlnvdWEB
News mentions
0No linked articles in our index yet.