From 38e8ae286160eb27620e7cb42108b3b28d1f299f Mon Sep 17 00:00:00 2001 From: Simon Pichugin Date: Wed, 8 Jul 2026 17:36:30 -0700 Subject: [PATCH] Merge commit from fork CVE: CVE-2026-59884 Upstream-Status: Backport [https://github.com/pyasn1/pyasn1/commit/628e36ecbb5277a3f01572ce418ef54271b165a5] (cherry picked from commit 628e36ecbb5277a3f01572ce418ef54271b165a5) Signed-off-by: Emily Vekariya --- pyasn1/codec/ber/decoder.py | 13 +++++++++++-- pyasn1/type/tag.py | 20 ++++++++++++++++---- tests/codec/ber/test_decoder.py | 25 +++++++++++++++++++++++++ tests/codec/cer/test_decoder.py | 15 +++++++++++++++ tests/codec/der/test_decoder.py | 15 +++++++++++++++ tests/type/test_tag.py | 20 ++++++++++++++++++++ 6 files changed, 102 insertions(+), 6 deletions(-) diff --git a/pyasn1/codec/ber/decoder.py b/pyasn1/codec/ber/decoder.py index be8ba65..18865c2 100644 --- a/pyasn1/codec/ber/decoder.py +++ b/pyasn1/codec/ber/decoder.py @@ -39,6 +39,10 @@ SubstrateUnderrunError = error.SubstrateUnderrunError # 20 octets allows up to 140-bit integers, supporting UUID-based OIDs MAX_OID_ARC_CONTINUATION_OCTETS = 20 +# 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 + class AbstractPayloadDecoder(object): protoComponent = None @@ -1570,7 +1574,7 @@ class SingleItemDecoder(object): if tagId == 0x1F: isShortTag = False - lengthOctetIdx = 0 + tagOctetCount = 0 tagId = 0 while True: @@ -1584,7 +1588,12 @@ class SingleItemDecoder(object): ) integerTag = ord(integerByte) - lengthOctetIdx += 1 + 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) diff --git a/pyasn1/type/tag.py b/pyasn1/type/tag.py index a21a405..bbbdd85 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 f6ff7b0..0152027 100644 --- a/tests/codec/ber/test_decoder.py +++ b/tests/codec/ber/test_decoder.py @@ -34,6 +34,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 error.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 3d27194..d759f76 100644 --- a/tests/codec/cer/test_decoder.py +++ b/tests/codec/cer/test_decoder.py @@ -67,6 +67,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 RealDecoderTestCase(BaseTestCase): def testLargeBinaryRoundTrip(self): substrate = encoder.encode(univ.Real((-1, 2, 76354972))) diff --git a/tests/codec/der/test_decoder.py b/tests/codec/der/test_decoder.py index 553563c..726c999 100644 --- a/tests/codec/der/test_decoder.py +++ b/tests/codec/der/test_decoder.py @@ -73,6 +73,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 RealDecoderTestCase(BaseTestCase): def testCanonicalLargeBinaryReal(self): substrate = encoder.encode(univ.Real((1, 2, 1000000))) diff --git a/tests/type/test_tag.py b/tests/type/test_tag.py index d0ffa07..ab9b8b1 100644 --- a/tests/type/test_tag.py +++ b/tests/type/test_tag.py @@ -9,6 +9,7 @@ import unittest from tests.base import BaseTestCase +from pyasn1 import error from pyasn1.type import tag @@ -23,6 +24,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): @@ -54,6 +68,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.34.1