Skip to content

Commit 94161a5

Browse files
CBL-Mariner-Botazurelinux-securityKanishk-Bansal
authored
Merge PR "[AUTO-CHERRYPICK] [AutoPR- Security] Patch python-pyasn1 for CVE-2026-30922 [HIGH] - branch main" #16333
Co-authored-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> Co-authored-by: Kanishk Bansal <103916909+Kanishk-Bansal@users.noreply.github.com>
1 parent 89ede4a commit 94161a5

File tree

2 files changed

+272
-2
lines changed

2 files changed

+272
-2
lines changed
Lines changed: 266 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
From 09021763382fa1f170c02d828ce03ee632d5eaf0 Mon Sep 17 00:00:00 2001
2+
From: AllSpark <allspark@microsoft.com>
3+
Date: Wed, 25 Mar 2026 09:19:45 +0000
4+
Subject: [PATCH] BER decoder: enforce maximum nesting depth to prevent
5+
excessively nested ASN.1 structures; add tests for BER/CER/DER decoders
6+
verifying depth limit and error messaging
7+
8+
Signed-off-by: Azure Linux Security Servicing Account <azurelinux-security@microsoft.com>
9+
Upstream-reference: AI Backport of https://github.com/pyasn1/pyasn1/commit/25ad481c19fdb006e20485ef3fc2e5b3eff30ef0.patch
10+
---
11+
pyasn1/codec/ber/decoder.py | 12 ++++
12+
tests/codec/ber/test_decoder.py | 117 ++++++++++++++++++++++++++++++++
13+
tests/codec/cer/test_decoder.py | 24 +++++++
14+
tests/codec/der/test_decoder.py | 43 ++++++++++++
15+
4 files changed, 196 insertions(+)
16+
17+
diff --git a/pyasn1/codec/ber/decoder.py b/pyasn1/codec/ber/decoder.py
18+
index 5ff485f..1844811 100644
19+
--- a/pyasn1/codec/ber/decoder.py
20+
+++ b/pyasn1/codec/ber/decoder.py
21+
@@ -23,6 +23,10 @@ LOG = debug.registerLoggee(__name__, flags=debug.DEBUG_DECODER)
22+
noValue = base.noValue
23+
24+
25+
+# Maximum nesting depth to protect against excessively nested ASN.1 structures
26+
+MAX_NESTING_DEPTH = 100
27+
+
28+
+
29+
class AbstractDecoder(object):
30+
protoComponent = None
31+
32+
@@ -1312,6 +1316,14 @@ class Decoder(object):
33+
if LOG:
34+
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)))
35+
36+
+
37+
+ _nestingLevel = options.get('_nestingLevel', 0)
38+
+ if _nestingLevel > MAX_NESTING_DEPTH:
39+
+ raise error.PyAsn1Error(
40+
+ 'ASN.1 structure nesting depth exceeds limit (%d)' % MAX_NESTING_DEPTH
41+
+ )
42+
+ options['_nestingLevel'] = _nestingLevel + 1
43+
+
44+
allowEoo = options.pop('allowEoo', False)
45+
46+
# Look for end-of-octets sentinel
47+
diff --git a/tests/codec/ber/test_decoder.py b/tests/codec/ber/test_decoder.py
48+
index e3b74df..f2a5975 100644
49+
--- a/tests/codec/ber/test_decoder.py
50+
+++ b/tests/codec/ber/test_decoder.py
51+
@@ -1614,6 +1614,123 @@ class ErrorOnDecodingTestCase(BaseTestCase):
52+
'Unexpected rest of substrate after raw dump %r' % rest)
53+
54+
55+
+
56+
+
57+
+class NestingDepthLimitTestCase(BaseTestCase):
58+
+ """Test protection against deeply nested ASN.1 structures (CVE prevention)."""
59+
+
60+
+ def testIndefLenSequenceNesting(self):
61+
+ """Deeply nested indefinite-length SEQUENCEs must raise PyAsn1Error."""
62+
+ # Each \x30\x80 opens a new indefinite-length SEQUENCE
63+
+ payload = b'\x30\x80' * 200
64+
+ try:
65+
+ decoder.decode(payload)
66+
+ except PyAsn1Error:
67+
+ pass
68+
+ else:
69+
+ assert False, 'Deeply nested indef-length SEQUENCEs not rejected'
70+
+
71+
+ def testIndefLenSetNesting(self):
72+
+ """Deeply nested indefinite-length SETs must raise PyAsn1Error."""
73+
+ # Each \x31\x80 opens a new indefinite-length SET
74+
+ payload = b'\x31\x80' * 200
75+
+ try:
76+
+ decoder.decode(payload)
77+
+ except PyAsn1Error:
78+
+ pass
79+
+ else:
80+
+ assert False, 'Deeply nested indef-length SETs not rejected'
81+
+
82+
+ def testDefiniteLenNesting(self):
83+
+ """Deeply nested definite-length SEQUENCEs must raise PyAsn1Error."""
84+
+ inner = b'\x05\x00' # NULL
85+
+ for _ in range(200):
86+
+ length = len(inner)
87+
+ if length < 128:
88+
+ inner = b'\x30' + bytes([length]) + inner
89+
+ else:
90+
+ length_bytes = length.to_bytes(
91+
+ (length.bit_length() + 7) // 8, 'big')
92+
+ inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
93+
+ length_bytes + inner
94+
+ try:
95+
+ decoder.decode(inner)
96+
+ except PyAsn1Error:
97+
+ pass
98+
+ else:
99+
+ assert False, 'Deeply nested definite-length SEQUENCEs not rejected'
100+
+
101+
+ def testNestingUnderLimitWorks(self):
102+
+ """Nesting within the limit must decode successfully."""
103+
+ inner = b'\x05\x00' # NULL
104+
+ for _ in range(50):
105+
+ length = len(inner)
106+
+ if length < 128:
107+
+ inner = b'\x30' + bytes([length]) + inner
108+
+ else:
109+
+ length_bytes = length.to_bytes(
110+
+ (length.bit_length() + 7) // 8, 'big')
111+
+ inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
112+
+ length_bytes + inner
113+
+ asn1Object, _ = decoder.decode(inner)
114+
+ assert asn1Object is not None, 'Valid nested structure rejected'
115+
+
116+
+ def testSiblingsDontIncreaseDepth(self):
117+
+ """Sibling elements at the same level must not inflate depth count."""
118+
+ # SEQUENCE containing 200 INTEGER siblings - should decode fine
119+
+ components = b'\x02\x01\x01' * 200 # 200 x INTEGER(1)
120+
+ length = len(components)
121+
+ length_bytes = length.to_bytes(
122+
+ (length.bit_length() + 7) // 8, 'big')
123+
+ payload = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
124+
+ length_bytes + components
125+
+ asn1Object, _ = decoder.decode(payload)
126+
+ assert asn1Object is not None, 'Siblings incorrectly rejected'
127+
+
128+
+ def testErrorMessageContainsLimit(self):
129+
+ """Error message must indicate the nesting depth limit."""
130+
+ payload = b'\x30\x80' * 200
131+
+ try:
132+
+ decoder.decode(payload)
133+
+ except PyAsn1Error as exc:
134+
+ assert 'nesting depth' in str(exc).lower(), \
135+
+ 'Error message missing depth info: %s' % exc
136+
+ else:
137+
+ assert False, 'Expected PyAsn1Error'
138+
+
139+
+ def testNoRecursionError(self):
140+
+ """Must raise PyAsn1Error, not RecursionError."""
141+
+ payload = b'\x30\x80' * 50000
142+
+ try:
143+
+ decoder.decode(payload)
144+
+ except PyAsn1Error:
145+
+ pass
146+
+ except RecursionError:
147+
+ assert False, 'Got RecursionError instead of PyAsn1Error'
148+
+
149+
+ def testMixedNesting(self):
150+
+ """Mixed SEQUENCE and SET nesting must be caught."""
151+
+ # Alternate SEQUENCE (0x30) and SET (0x31) with indef length
152+
+ payload = b''
153+
+ for i in range(200):
154+
+ payload += b'\x30\x80' if i % 2 == 0 else b'\x31\x80'
155+
+ try:
156+
+ decoder.decode(payload)
157+
+ except PyAsn1Error:
158+
+ pass
159+
+ else:
160+
+ assert False, 'Mixed nesting not rejected'
161+
+
162+
+ def testWithSchema(self):
163+
+ """Deeply nested structures must be caught even with schema."""
164+
+ payload = b'\x30\x80' * 200
165+
+ try:
166+
+ decoder.decode(payload, asn1Spec=univ.Sequence())
167+
+ except PyAsn1Error:
168+
+ pass
169+
+ else:
170+
+ assert False, 'Deeply nested with schema not rejected'
171+
+
172+
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
173+
174+
if __name__ == '__main__':
175+
diff --git a/tests/codec/cer/test_decoder.py b/tests/codec/cer/test_decoder.py
176+
index bb5ce93..5064f92 100644
177+
--- a/tests/codec/cer/test_decoder.py
178+
+++ b/tests/codec/cer/test_decoder.py
179+
@@ -367,6 +367,30 @@ class SequenceDecoderWithExplicitlyTaggedSetOfOpenTypesTestCase(BaseTestCase):
180+
assert s[1][0] == univ.OctetString(hexValue='02010C')
181+
182+
183+
+
184+
+class NestingDepthLimitTestCase(BaseTestCase):
185+
+ """Test CER decoder protection against deeply nested structures."""
186+
+
187+
+ def testIndefLenNesting(self):
188+
+ """Deeply nested indefinite-length SEQUENCEs must raise PyAsn1Error."""
189+
+ payload = b'\x30\x80' * 200
190+
+ try:
191+
+ decoder.decode(payload)
192+
+ except PyAsn1Error:
193+
+ pass
194+
+ else:
195+
+ assert False, 'Deeply nested indef-length SEQUENCEs not rejected'
196+
+
197+
+ def testNoRecursionError(self):
198+
+ """Must raise PyAsn1Error, not RecursionError."""
199+
+ payload = b'\x30\x80' * 50000
200+
+ try:
201+
+ decoder.decode(payload)
202+
+ except PyAsn1Error:
203+
+ pass
204+
+ except RecursionError:
205+
+ assert False, 'Got RecursionError instead of PyAsn1Error'
206+
+
207+
suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
208+
209+
if __name__ == '__main__':
210+
diff --git a/tests/codec/der/test_decoder.py b/tests/codec/der/test_decoder.py
211+
index 51ce296..53ffd9a 100644
212+
--- a/tests/codec/der/test_decoder.py
213+
+++ b/tests/codec/der/test_decoder.py
214+
@@ -119,6 +119,49 @@ class SequenceDecoderWithUntaggedOpenTypesTestCase(BaseTestCase):
215+
except PyAsn1Error:
216+
pass
217+
218+
+
219+
+class NestingDepthLimitTestCase(BaseTestCase):
220+
+ """Test DER decoder protection against deeply nested structures."""
221+
+
222+
+ def testDefiniteLenNesting(self):
223+
+ """Deeply nested definite-length SEQUENCEs must raise PyAsn1Error."""
224+
+ inner = b'\x05\x00' # NULL
225+
+ for _ in range(200):
226+
+ length = len(inner)
227+
+ if length < 128:
228+
+ inner = b'\x30' + bytes([length]) + inner
229+
+ else:
230+
+ length_bytes = length.to_bytes(
231+
+ (length.bit_length() + 7) // 8, 'big')
232+
+ inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
233+
+ length_bytes + inner
234+
+ try:
235+
+ decoder.decode(inner)
236+
+ except PyAsn1Error:
237+
+ pass
238+
+ else:
239+
+ assert False, 'Deeply nested definite-length SEQUENCEs not rejected'
240+
+
241+
+ def testNoRecursionError(self):
242+
+ """Must raise PyAsn1Error, not RecursionError."""
243+
+ inner = b'\x05\x00'
244+
+ for _ in range(200):
245+
+ length = len(inner)
246+
+ if length < 128:
247+
+ inner = b'\x30' + bytes([length]) + inner
248+
+ else:
249+
+ length_bytes = length.to_bytes(
250+
+ (length.bit_length() + 7) // 8, 'big')
251+
+ inner = b'\x30' + bytes([0x80 | len(length_bytes)]) + \
252+
+ length_bytes + inner
253+
+ try:
254+
+ decoder.decode(inner)
255+
+ except PyAsn1Error:
256+
+ pass
257+
+ except RecursionError:
258+
+ assert False, 'Got RecursionError instead of PyAsn1Error'
259+
+
260+
+
261+
else:
262+
assert False, 'unknown open type tolerated'
263+
264+
--
265+
2.45.4
266+

SPECS/python-pyasn1/python-pyasn1.spec

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
Summary: Implementation of ASN.1 types and codecs in Python programming language
22
Name: python-pyasn1
33
Version: 0.4.8
4-
Release: 1%{?dist}
4+
Release: 2%{?dist}
55
License: BSD
66
Vendor: Microsoft Corporation
77
Distribution: Mariner
88
Group: Development/Languages/Python
99
URL: https://pypi.org/project/pyasn1
1010
Source0: https://files.pythonhosted.org/packages/source/p/pyasn1/pyasn1-%{version}.tar.gz
11+
Patch0: CVE-2026-30922.patch
1112
BuildArch: noarch
1213

1314
%description
@@ -24,7 +25,7 @@ It has been first written to support particular protocol (SNMP) but then general
2425
to be suitable for a wide range of protocols based on ASN.1 specification.
2526

2627
%prep
27-
%autosetup -n pyasn1-%{version}
28+
%autosetup -p1 -n pyasn1-%{version}
2829

2930
%build
3031
%py3_build
@@ -41,6 +42,9 @@ to be suitable for a wide range of protocols based on ASN.1 specification.
4142
%{python3_sitelib}/*
4243

4344
%changelog
45+
* Wed Mar 25 2026 Azure Linux Security Servicing Account <azurelinux-security@microsoft.com> - 0.4.8-2
46+
- Patch for CVE-2026-30922
47+
4448
* Mon Jan 03 2022 Thomas Crain <thcrain@microsoft.com> - 0.4.8-1
4549
- Upgrade to latest upstream version
4650
- Use nicer Source0

0 commit comments

Comments
 (0)