Skip to content

Commit 3908f14

Browse files
committed
Merge commit from fork
Add limit of 20 continuation octets per OID arc to prevent a potential memory exhaustion from excessive continuation bytes input.
1 parent 0a7e067 commit 3908f14

2 files changed

Lines changed: 149 additions & 1 deletion

File tree

pyasn1/codec/ber/decoder.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333

3434
SubstrateUnderrunError = error.SubstrateUnderrunError
3535

36+
# Maximum number of continuation octets (high-bit set) allowed per OID arc.
37+
# 20 octets allows up to 140-bit integers, supporting UUID-based OIDs
38+
MAX_OID_ARC_CONTINUATION_OCTETS = 20
39+
3640

3741
class AbstractPayloadDecoder(object):
3842
protoComponent = None
@@ -427,7 +431,14 @@ def valueDecoder(self, substrate, asn1Spec,
427431
# Construct subid from a number of octets
428432
nextSubId = subId
429433
subId = 0
434+
continuationOctetCount = 0
430435
while nextSubId >= 128:
436+
continuationOctetCount += 1
437+
if continuationOctetCount > MAX_OID_ARC_CONTINUATION_OCTETS:
438+
raise error.PyAsn1Error(
439+
'OID arc exceeds maximum continuation octets limit (%d) '
440+
'at position %d' % (MAX_OID_ARC_CONTINUATION_OCTETS, index)
441+
)
431442
subId = (subId << 7) + (nextSubId & 0x7F)
432443
if index >= substrateLen:
433444
raise error.SubstrateUnderrunError(
@@ -485,7 +496,14 @@ def valueDecoder(self, substrate, asn1Spec,
485496
# Construct subid from a number of octets
486497
nextSubId = subId
487498
subId = 0
499+
continuationOctetCount = 0
488500
while nextSubId >= 128:
501+
continuationOctetCount += 1
502+
if continuationOctetCount > MAX_OID_ARC_CONTINUATION_OCTETS:
503+
raise error.PyAsn1Error(
504+
'RELATIVE-OID arc exceeds maximum continuation octets limit (%d) '
505+
'at position %d' % (MAX_OID_ARC_CONTINUATION_OCTETS, index)
506+
)
489507
subId = (subId << 7) + (nextSubId & 0x7F)
490508
if index >= substrateLen:
491509
raise error.SubstrateUnderrunError(
@@ -1915,7 +1933,7 @@ class StreamingDecoder(object):
19151933
:py:class:`~pyasn1.error.SubstrateUnderrunError` object indicating
19161934
insufficient BER/CER/DER serialization on input to fully recover ASN.1
19171935
objects from it.
1918-
1936+
19191937
In the latter case the caller is advised to ensure some more data in
19201938
the input stream, then call the iterator again. The decoder will resume
19211939
the decoding process using the newly arrived data.

tests/codec/ber/test_decoder.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,72 @@ def testLarge2(self):
449449
bytes((0x06, 0x13, 0x88, 0x37, 0x83, 0xC6, 0xDF, 0xD4, 0xCC, 0xB3, 0xFF, 0xFF, 0xFE, 0xF0, 0xB8, 0xD6, 0xB8, 0xCB, 0xE2, 0xB6, 0x47))
450450
) == ((2, 999, 18446744073709551535184467440737095), b'')
451451

452+
def testExcessiveContinuationOctets(self):
453+
"""Test that OID arcs with excessive continuation octets are rejected."""
454+
# Create a payload with 25 continuation octets (exceeds 20 limit)
455+
# 0x81 bytes are continuation octets, 0x01 terminates
456+
malicious_payload = bytes([0x06, 26]) + bytes([0x81] * 25) + bytes([0x01])
457+
try:
458+
decoder.decode(malicious_payload)
459+
except error.PyAsn1Error:
460+
pass
461+
else:
462+
assert 0, 'Excessive continuation octets tolerated'
463+
464+
def testMaxAllowedContinuationOctets(self):
465+
"""Test that OID arcs at the maximum continuation octets limit work."""
466+
# Create a payload with exactly 20 continuation octets (at limit)
467+
# This should succeed
468+
payload = bytes([0x06, 21]) + bytes([0x81] * 20) + bytes([0x01])
469+
try:
470+
decoder.decode(payload)
471+
except error.PyAsn1Error:
472+
assert 0, 'Valid OID with 20 continuation octets rejected'
473+
474+
def testOneOverContinuationLimit(self):
475+
"""Test boundary: 21 continuation octets (one over limit) is rejected."""
476+
payload = bytes([0x06, 22]) + bytes([0x81] * 21) + bytes([0x01])
477+
try:
478+
decoder.decode(payload)
479+
except error.PyAsn1Error:
480+
pass
481+
else:
482+
assert 0, '21 continuation octets tolerated (should be rejected)'
483+
484+
def testExcessiveContinuationInSecondArc(self):
485+
"""Test that limit applies to subsequent arcs, not just the first."""
486+
# First arc: valid simple byte (0x55 = 85, decodes to arc 2.5)
487+
# Second arc: excessive continuation octets
488+
payload = bytes([0x06, 27]) + bytes([0x55]) + bytes([0x81] * 25) + bytes([0x01])
489+
try:
490+
decoder.decode(payload)
491+
except error.PyAsn1Error:
492+
pass
493+
else:
494+
assert 0, 'Excessive continuation in second arc tolerated'
495+
496+
def testMultipleArcsAtLimit(self):
497+
"""Test multiple arcs each at the continuation limit work correctly."""
498+
# Two arcs, each with 20 continuation octets (both at limit)
499+
arc1 = bytes([0x81] * 20) + bytes([0x01]) # 21 bytes
500+
arc2 = bytes([0x81] * 20) + bytes([0x01]) # 21 bytes
501+
payload = bytes([0x06, 42]) + arc1 + arc2
502+
try:
503+
decoder.decode(payload)
504+
except error.PyAsn1Error:
505+
assert 0, 'Multiple valid arcs at limit rejected'
506+
507+
def testExcessiveContinuationWithMaxBytes(self):
508+
"""Test with 0xFF continuation bytes (maximum value, not just 0x81)."""
509+
# 0xFF bytes are also continuation octets (high bit set)
510+
malicious_payload = bytes([0x06, 26]) + bytes([0xFF] * 25) + bytes([0x01])
511+
try:
512+
decoder.decode(malicious_payload)
513+
except error.PyAsn1Error:
514+
pass
515+
else:
516+
assert 0, 'Excessive 0xFF continuation octets tolerated'
517+
452518

453519
class RelativeOIDDecoderTestCase(BaseTestCase):
454520
def testOne(self):
@@ -518,6 +584,70 @@ def testLarge(self):
518584
bytes((0x0D, 0x13, 0x88, 0x37, 0x83, 0xC6, 0xDF, 0xD4, 0xCC, 0xB3, 0xFF, 0xFF, 0xFE, 0xF0, 0xB8, 0xD6, 0xB8, 0xCB, 0xE2, 0xB6, 0x47))
519585
) == ((1079, 18446744073709551535184467440737095), b'')
520586

587+
def testExcessiveContinuationOctets(self):
588+
"""Test that RELATIVE-OID arcs with excessive continuation octets are rejected."""
589+
# Create a payload with 25 continuation octets (exceeds 20 limit)
590+
malicious_payload = bytes([0x0D, 26]) + bytes([0x81] * 25) + bytes([0x01])
591+
try:
592+
decoder.decode(malicious_payload)
593+
except error.PyAsn1Error:
594+
pass
595+
else:
596+
assert 0, 'Excessive continuation octets tolerated'
597+
598+
def testMaxAllowedContinuationOctets(self):
599+
"""Test that RELATIVE-OID arcs at the maximum continuation octets limit work."""
600+
# Create a payload with exactly 20 continuation octets (at limit)
601+
payload = bytes([0x0D, 21]) + bytes([0x81] * 20) + bytes([0x01])
602+
try:
603+
decoder.decode(payload)
604+
except error.PyAsn1Error:
605+
assert 0, 'Valid RELATIVE-OID with 20 continuation octets rejected'
606+
607+
def testOneOverContinuationLimit(self):
608+
"""Test boundary: 21 continuation octets (one over limit) is rejected."""
609+
payload = bytes([0x0D, 22]) + bytes([0x81] * 21) + bytes([0x01])
610+
try:
611+
decoder.decode(payload)
612+
except error.PyAsn1Error:
613+
pass
614+
else:
615+
assert 0, '21 continuation octets tolerated (should be rejected)'
616+
617+
def testExcessiveContinuationInSecondArc(self):
618+
"""Test that limit applies to subsequent arcs, not just the first."""
619+
# First arc: valid simple byte
620+
# Second arc: excessive continuation octets
621+
payload = bytes([0x0D, 27]) + bytes([0x55]) + bytes([0x81] * 25) + bytes([0x01])
622+
try:
623+
decoder.decode(payload)
624+
except error.PyAsn1Error:
625+
pass
626+
else:
627+
assert 0, 'Excessive continuation in second arc tolerated'
628+
629+
def testMultipleArcsAtLimit(self):
630+
"""Test multiple arcs each at the continuation limit work correctly."""
631+
# Two arcs, each with 20 continuation octets (both at limit)
632+
arc1 = bytes([0x81] * 20) + bytes([0x01]) # 21 bytes
633+
arc2 = bytes([0x81] * 20) + bytes([0x01]) # 21 bytes
634+
payload = bytes([0x0D, 42]) + arc1 + arc2
635+
try:
636+
decoder.decode(payload)
637+
except error.PyAsn1Error:
638+
assert 0, 'Multiple valid arcs at limit rejected'
639+
640+
def testExcessiveContinuationWithMaxBytes(self):
641+
"""Test with 0xFF continuation bytes (maximum value, not just 0x81)."""
642+
# 0xFF bytes are also continuation octets (high bit set)
643+
malicious_payload = bytes([0x0D, 26]) + bytes([0xFF] * 25) + bytes([0x01])
644+
try:
645+
decoder.decode(malicious_payload)
646+
except error.PyAsn1Error:
647+
pass
648+
else:
649+
assert 0, 'Excessive 0xFF continuation octets tolerated'
650+
521651

522652
class RealDecoderTestCase(BaseTestCase):
523653
def testChar(self):

0 commit comments

Comments
 (0)