Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 20 additions & 19 deletions SPECS/python-pyasn1/CVE-2026-30922.patch
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ Subject: [PATCH] BER decoder: enforce maximum nesting depth to prevent

Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
Upstream-reference: AI Backport of https://github.com/pyasn1/pyasn1/commit/25ad481c19fdb006e20485ef3fc2e5b3eff30ef0.patch

---
pyasn1/codec/ber/decoder.py | 12 ++++
tests/codec/ber/test_decoder.py | 117 ++++++++++++++++++++++++++++++++
pyasn1/codec/ber/decoder.py | 14 ++++
tests/codec/ber/test_decoder.py | 116 ++++++++++++++++++++++++++++++++
tests/codec/cer/test_decoder.py | 24 +++++++
tests/codec/der/test_decoder.py | 43 ++++++++++++
tests/codec/der/test_decoder.py | 42 ++++++++++++
4 files changed, 196 insertions(+)

diff --git a/pyasn1/codec/ber/decoder.py b/pyasn1/codec/ber/decoder.py
index 5ff485f..1844811 100644
index 5ff485f..49c0ed6 100644
--- a/pyasn1/codec/ber/decoder.py
+++ b/pyasn1/codec/ber/decoder.py
@@ -23,6 +23,10 @@ LOG = debug.registerLoggee(__name__, flags=debug.DEBUG_DECODER)
Expand All @@ -29,31 +30,31 @@ index 5ff485f..1844811 100644
class AbstractDecoder(object):
protoComponent = None

@@ -1312,6 +1316,14 @@ class Decoder(object):
@@ -1312,6 +1316,16 @@ class Decoder(object):
if LOG:
LOG('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
diff --git a/tests/codec/ber/test_decoder.py b/tests/codec/ber/test_decoder.py
index e3b74df..f2a5975 100644
index e3b74df..450e78d 100644
--- a/tests/codec/ber/test_decoder.py
+++ b/tests/codec/ber/test_decoder.py
@@ -1614,6 +1614,123 @@ class ErrorOnDecodingTestCase(BaseTestCase):
@@ -1614,6 +1614,122 @@ class ErrorOnDecodingTestCase(BaseTestCase):
'Unexpected rest of substrate after raw dump %r' % rest)


+
+
+class NestingDepthLimitTestCase(BaseTestCase):
+ """Test protection against deeply nested ASN.1 structures (CVE prevention)."""
+
Expand Down Expand Up @@ -168,19 +169,19 @@ index e3b74df..f2a5975 100644
+ pass
+ else:
+ assert False, 'Deeply nested with schema not rejected'
+
+
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])

if __name__ == '__main__':
diff --git a/tests/codec/cer/test_decoder.py b/tests/codec/cer/test_decoder.py
index bb5ce93..5064f92 100644
index bb5ce93..50facd5 100644
--- a/tests/codec/cer/test_decoder.py
+++ b/tests/codec/cer/test_decoder.py
@@ -367,6 +367,30 @@ class SequenceDecoderWithExplicitlyTaggedSetOfOpenTypesTestCase(BaseTestCase):
assert s[1][0] == univ.OctetString(hexValue='02010C')


+
+class NestingDepthLimitTestCase(BaseTestCase):
+ """Test CER decoder protection against deeply nested structures."""
+
Expand All @@ -203,19 +204,19 @@ index bb5ce93..5064f92 100644
+ pass
+ except RecursionError:
+ assert False, 'Got RecursionError instead of PyAsn1Error'
+
+
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])

if __name__ == '__main__':
diff --git a/tests/codec/der/test_decoder.py b/tests/codec/der/test_decoder.py
index 51ce296..53ffd9a 100644
index 51ce296..ae4416d 100644
--- a/tests/codec/der/test_decoder.py
+++ b/tests/codec/der/test_decoder.py
@@ -119,6 +119,49 @@ class SequenceDecoderWithUntaggedOpenTypesTestCase(BaseTestCase):
except PyAsn1Error:
pass
@@ -367,6 +367,48 @@ class SequenceDecoderWithExplicitlyTaggedSetOfOpenTypesTestCase(BaseTestCase):
assert s[1][0] == univ.OctetString(hexValue='02010C')


+
+class NestingDepthLimitTestCase(BaseTestCase):
+ """Test DER decoder protection against deeply nested structures."""
+
Expand Down Expand Up @@ -258,9 +259,9 @@ index 51ce296..53ffd9a 100644
+ assert False, 'Got RecursionError instead of PyAsn1Error'
+
+
else:
assert False, 'unknown open type tolerated'
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])

if __name__ == '__main__':
--
2.45.4
2.34.1

271 changes: 271 additions & 0 deletions SPECS/python-pyasn1/CVE-2026-59884.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
From 628e36ecbb5277a3f01572ce418ef54271b165a5 Mon Sep 17 00:00:00 2001
From: Simon Pichugin <simon.pichugin@gmail.com>
Date: Wed, 8 Jul 2026 17:36:30 -0700
Subject: [PATCH] Merge commit from fork

Upstream Patch reference: https://github.com/pyasn1/pyasn1/commit/628e36ecbb5277a3f01572ce418ef54271b165a5.patch
---
pyasn1/codec/ber/decoder.py | 17 +++++++++++++----
pyasn1/type/tag.py | 20 ++++++++++++++++----
tests/codec/ber/test_decoder.py | 26 ++++++++++++++++++++++++++
tests/codec/cer/test_decoder.py | 16 ++++++++++++++++
tests/codec/der/test_decoder.py | 16 ++++++++++++++++
tests/type/test_tag.py | 20 ++++++++++++++++++++
6 files changed, 107 insertions(+), 8 deletions(-)

diff --git a/pyasn1/codec/ber/decoder.py b/pyasn1/codec/ber/decoder.py
index 1844811..2cd8a82 100644
--- a/pyasn1/codec/ber/decoder.py
+++ b/pyasn1/codec/ber/decoder.py
@@ -24,6 +24,10 @@ noValue = base.noValue


# Maximum nesting depth to protect against excessively nested ASN.1 structures
+
+# Maximum number of octets in a long-form tag ID (20 octets = up to
+# 140-bit tag IDs, matching the OID arc limit)
+MAX_TAG_OCTETS = 20
MAX_NESTING_DEPTH = 100


@@ -1366,19 +1370,24 @@ class Decoder(object):

if tagId == 0x1F:
isShortTag = False
- lengthOctetIdx = 0
+ tagOctetCount = 0
tagId = 0

try:
while True:
- integerTag = oct2int(substrate[lengthOctetIdx])
- lengthOctetIdx += 1
+ integerTag = oct2int(substrate[tagOctetCount])
+ tagOctetCount += 1
+ if tagOctetCount > MAX_TAG_OCTETS:
+ raise error.PyAsn1Error(
+ 'Tag ID octet count exceeds limit (%d)' % (
+ MAX_TAG_OCTETS,)
+ )
tagId <<= 7
tagId |= (integerTag & 0x7F)
if not integerTag & 0x80:
break

- substrate = substrate[lengthOctetIdx:]
+ substrate = substrate[tagOctetCount:]

except IndexError:
raise error.SubstrateUnderrunError(
diff --git a/pyasn1/type/tag.py b/pyasn1/type/tag.py
index b88a734..a9811c4 100644
--- a/pyasn1/type/tag.py
+++ b/pyasn1/type/tag.py
@@ -34,6 +34,16 @@ tagCategoryExplicit = 0x02
tagCategoryUntagged = 0x04


+def _tagIdToStr(tagId):
+ # Decimal rendering of a huge tag ID can exceed the interpreter's
+ # integer-to-string conversion limit (sys.get_int_max_str_digits(),
+ # Python 3.11+) and raise ValueError; hexadecimal is not limited
+ try:
+ return str(tagId)
+ except ValueError:
+ return hex(tagId)
+
+
class Tag(object):
"""Create ASN.1 tag

@@ -56,7 +66,8 @@ class Tag(object):
"""
def __init__(self, tagClass, tagFormat, tagId):
if tagId < 0:
- raise error.PyAsn1Error('Negative tag ID (%s) not allowed' % tagId)
+ raise error.PyAsn1Error(
+ 'Negative tag ID (%s) not allowed' % _tagIdToStr(tagId))
self.__tagClass = tagClass
self.__tagFormat = tagFormat
self.__tagId = tagId
@@ -65,7 +76,7 @@ class Tag(object):

def __repr__(self):
representation = '[%s:%s:%s]' % (
- self.__tagClass, self.__tagFormat, self.__tagId)
+ self.__tagClass, self.__tagFormat, _tagIdToStr(self.__tagId))
return '<%s object, tag %s>' % (
self.__class__.__name__, representation)

@@ -194,8 +205,9 @@ class TagSet(object):
self.__hash = hash(self.__superTagsClassId)

def __repr__(self):
- representation = '-'.join(['%s:%s:%s' % (x.tagClass, x.tagFormat, x.tagId)
- for x in self.__superTags])
+ representation = '-'.join(
+ ['%s:%s:%s' % (x.tagClass, x.tagFormat, _tagIdToStr(x.tagId))
+ for x in self.__superTags])
if representation:
representation = 'tags ' + representation
else:
diff --git a/tests/codec/ber/test_decoder.py b/tests/codec/ber/test_decoder.py
index f2a5975..8c11e26 100644
--- a/tests/codec/ber/test_decoder.py
+++ b/tests/codec/ber/test_decoder.py
@@ -20,6 +20,7 @@ from pyasn1.type import opentype
from pyasn1.type import univ
from pyasn1.type import char
from pyasn1.codec.ber import decoder
+from pyasn1.codec.ber import encoder
from pyasn1.codec.ber import eoo
from pyasn1.compat.octets import ints2octs, str2octs, null
from pyasn1.error import PyAsn1Error
@@ -32,6 +33,31 @@ class LargeTagDecoderTestCase(BaseTestCase):
def testLongTag(self):
assert decoder.decode(ints2octs((0x1f, 2, 1, 0)))[0].tagSet == univ.Integer.tagSet

+ def testVeryLongTagRoundTrip(self):
+ # (1 << 140) - 1 is the largest tag ID fitting the 20 octet limit
+ for tagId in (1 << 77, (1 << 140) - 1):
+ largeTag = tag.Tag(tag.tagClassContext, tag.tagFormatSimple, tagId)
+ asn1Spec = univ.Integer().subtype(implicitTag=largeTag)
+ value = univ.Integer(1).subtype(implicitTag=largeTag)
+
+ decoded, rest = decoder.decode(encoder.encode(value), asn1Spec=asn1Spec)
+
+ assert rest == b''
+ assert decoded == 1
+
+ def testExcessiveLongTag(self):
+ # 1 << 140 is the smallest tag ID needing 21 octets, one over the limit
+ excessiveTag = tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1 << 140)
+ asn1Spec = univ.Integer().subtype(implicitTag=excessiveTag)
+ substrate = encoder.encode(univ.Integer(1).subtype(implicitTag=excessiveTag))
+
+ try:
+ decoder.decode(substrate, asn1Spec=asn1Spec)
+ except PyAsn1Error:
+ pass
+ else:
+ assert 0, 'excessive long tag tolerated'
+
def testTagsEquivalence(self):
integer = univ.Integer(2).subtype(implicitTag=tag.Tag(tag.tagClassContext, 0, 0))
assert decoder.decode(ints2octs((0x9f, 0x80, 0x00, 0x02, 0x01, 0x02)), asn1Spec=integer) == decoder.decode(
diff --git a/tests/codec/cer/test_decoder.py b/tests/codec/cer/test_decoder.py
index 5064f92..927e1f4 100644
--- a/tests/codec/cer/test_decoder.py
+++ b/tests/codec/cer/test_decoder.py
@@ -18,6 +18,7 @@ from pyasn1.type import namedtype
from pyasn1.type import opentype
from pyasn1.type import univ
from pyasn1.codec.cer import decoder
+from pyasn1.codec.cer import encoder
from pyasn1.compat.octets import ints2octs, str2octs, null
from pyasn1.error import PyAsn1Error

@@ -69,6 +70,21 @@ class OctetStringDecoderTestCase(BaseTestCase):
# TODO: test failures on short chunked and long unchunked substrate samples


+class LargeTagDecoderTestCase(BaseTestCase):
+ def testExcessiveLongTag(self):
+ # 1 << 140 is the smallest tag ID needing 21 octets, one over the limit
+ excessiveTag = tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1 << 140)
+ asn1Spec = univ.Integer().subtype(implicitTag=excessiveTag)
+ substrate = encoder.encode(univ.Integer(1).subtype(implicitTag=excessiveTag))
+
+ try:
+ decoder.decode(substrate, asn1Spec=asn1Spec)
+ except PyAsn1Error:
+ pass
+ else:
+ assert 0, 'excessive long tag tolerated'
+
+
class SequenceDecoderWithUntaggedOpenTypesTestCase(BaseTestCase):
def setUp(self):
openType = opentype.OpenType(
diff --git a/tests/codec/der/test_decoder.py b/tests/codec/der/test_decoder.py
index 53ffd9a..c2908c0 100644
--- a/tests/codec/der/test_decoder.py
+++ b/tests/codec/der/test_decoder.py
@@ -19,6 +19,7 @@ from pyasn1.type import namedtype
from pyasn1.type import opentype
from pyasn1.type import univ
from pyasn1.codec.der import decoder
+from pyasn1.codec.der import encoder
from pyasn1.compat.octets import ints2octs, null
from pyasn1.error import PyAsn1Error

@@ -77,6 +78,21 @@ class OctetStringDecoderTestCase(BaseTestCase):
assert 0, 'chunked encoding tolerated'


+class LargeTagDecoderTestCase(BaseTestCase):
+ def testExcessiveLongTag(self):
+ # 1 << 140 is the smallest tag ID needing 21 octets, one over the limit
+ excessiveTag = tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1 << 140)
+ asn1Spec = univ.Integer().subtype(implicitTag=excessiveTag)
+ substrate = encoder.encode(univ.Integer(1).subtype(implicitTag=excessiveTag))
+
+ try:
+ decoder.decode(substrate, asn1Spec=asn1Spec)
+ except PyAsn1Error:
+ pass
+ else:
+ assert 0, 'excessive long tag tolerated'
+
+
class SequenceDecoderWithUntaggedOpenTypesTestCase(BaseTestCase):
def setUp(self):
openType = opentype.OpenType(
diff --git a/tests/type/test_tag.py b/tests/type/test_tag.py
index e8dd7a3..d737c94 100644
--- a/tests/type/test_tag.py
+++ b/tests/type/test_tag.py
@@ -14,6 +14,7 @@ except ImportError:

from tests.base import BaseTestCase

+from pyasn1 import error
from pyasn1.type import tag


@@ -28,6 +29,19 @@ class TagReprTestCase(TagTestCaseBase):
def testRepr(self):
assert 'Tag' in repr(self.t1)

+ def testReprHugeTagId(self):
+ # must not hit the interpreter's int-to-str conversion limit
+ hugeTag = tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1 << 100000)
+ assert 'Tag' in repr(hugeTag)
+
+ def testNegativeHugeTagId(self):
+ try:
+ tag.Tag(tag.tagClassContext, tag.tagFormatSimple, -(1 << 100000))
+ except error.PyAsn1Error:
+ pass
+ else:
+ assert 0, 'negative tag ID tolerated'
+

class TagCmpTestCase(TagTestCaseBase):
def testCmp(self):
@@ -59,6 +73,12 @@ class TagSetReprTestCase(TagSetTestCaseBase):
def testRepr(self):
assert 'TagSet' in repr(self.ts1)

+ def testReprHugeTagId(self):
+ # must not hit the interpreter's int-to-str conversion limit
+ hugeTagSet = self.ts1.tagImplicitly(
+ tag.Tag(tag.tagClassContext, tag.tagFormatSimple, 1 << 100000))
+ assert 'TagSet' in repr(hugeTagSet)
+

class TagSetCmpTestCase(TagSetTestCaseBase):
def testCmp(self):
--
2.43.0

Loading
Loading