Skip to content

Commit 5a3cfb5

Browse files
committed
fix(ipv6): CALIPSO make() writes a corrupt length on overflow and raises raw errors for a bad pad
_make_opt_calipso in HOPOPT and IPv6-Opts now raises ProtocolError when the bitmap plus pad would exceed the 255 octets Opt Data Len can count, when pad is not bytes/bytearray, and when the bitmap is not a sequence of octets. Valid input is built unchanged. _read_opt_calipso no longer rejects an odd Cmpt Length, which RFC 5570 section 5.2 permits and make() builds; it now rejects a Cmpt Length that Opt Data Len does not cover. Closes #1408
1 parent b9660b4 commit 5a3cfb5

3 files changed

Lines changed: 182 additions & 6 deletions

File tree

‎pcapkit/protocols/internet/hopopt.py‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,9 @@ def _read_opt_calipso(self, schema: 'Schema_CALIPSOOption', *, options: 'Option'
755755
"""
756756
if schema.len < 8 and schema.len % 8 != 0:
757757
raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format')
758-
if schema.cmpt_len % 2 != 0:
758+
# ``Cmpt Length`` counts 32-bit words and may be odd [:rfc:`5570#section-5.2`];
759+
# it only has to fit inside ``Opt Data Len``
760+
if schema.len < 8 + schema.cmpt_len * 4:
759761
raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format')
760762

761763
opt = Data_CALIPSOOption(
@@ -1594,7 +1596,10 @@ def _make_opt_calipso(self, code: 'Enum_Option', opt: 'Optional[Data_CALIPSOOpti
15941596
Constructed option schema.
15951597
15961598
Raises:
1597-
ProtocolError: If the bitmap is not a whole number of 32-bit words.
1599+
ProtocolError: If the bitmap is not a sequence of octets or not a
1600+
whole number of 32-bit words, if ``pad`` is not :obj:`bytes`,
1601+
or if the option data would exceed the 255 octets
1602+
``Opt Data Len`` can count.
15981603
15991604
Note:
16001605
``Cmpt Length`` on the wire counts 32-bit words [:rfc:`5570#section-5.1`],
@@ -1607,10 +1612,26 @@ def _make_opt_calipso(self, code: 'Enum_Option', opt: 'Optional[Data_CALIPSOOpti
16071612
checksum = opt.checksum
16081613
bitmap = opt.cmpt_bitmap if hasattr(opt, 'cmpt_bitmap') else None
16091614
pad = opt.pad
1610-
bitmap_bytes = bytes(bitmap) if bitmap is not None else b''
1615+
if bitmap is None:
1616+
bitmap_bytes = b''
1617+
elif isinstance(bitmap, (bytes, bytearray, tuple, list)):
1618+
try:
1619+
bitmap_bytes = bytes(bitmap)
1620+
except (TypeError, ValueError) as exc:
1621+
raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid CALIPSO bitmap: '
1622+
f'{exc}') from exc
1623+
else:
1624+
raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid CALIPSO bitmap type: '
1625+
f'{type(bitmap).__name__}')
16111626
if len(bitmap_bytes) % 4 != 0:
16121627
raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid CALIPSO bitmap length: '
16131628
f'{len(bitmap_bytes)}')
1629+
if not isinstance(pad, (bytes, bytearray)):
1630+
raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid CALIPSO pad type: '
1631+
f'{type(pad).__name__}')
1632+
if 8 + len(bitmap_bytes) + len(pad) > 255:
1633+
raise ProtocolError(f'{self.alias}: [OptNo {code}] too long CALIPSO option data: '
1634+
f'{8 + len(bitmap_bytes) + len(pad)}')
16141635

16151636
return Schema_CALIPSOOption(
16161637
type=code,

‎pcapkit/protocols/internet/ipv6_opts.py‎

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -744,7 +744,9 @@ def _read_opt_calipso(self, schema: 'Schema_CALIPSOOption', *, options: 'Option'
744744
"""
745745
if schema.len < 8 and schema.len % 8 != 0:
746746
raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format')
747-
if schema.cmpt_len % 2 != 0:
747+
# ``Cmpt Length`` counts 32-bit words and may be odd [:rfc:`5570#section-5.2`];
748+
# it only has to fit inside ``Opt Data Len``
749+
if schema.len < 8 + schema.cmpt_len * 4:
748750
raise ProtocolError(f'{self.alias}: [OptNo {schema.type}] invalid format')
749751

750752
opt = Data_CALIPSOOption(
@@ -1583,7 +1585,10 @@ def _make_opt_calipso(self, code: 'Enum_Option', opt: 'Optional[Data_CALIPSOOpti
15831585
Constructed option schema.
15841586
15851587
Raises:
1586-
ProtocolError: If the bitmap is not a whole number of 32-bit words.
1588+
ProtocolError: If the bitmap is not a sequence of octets or not a
1589+
whole number of 32-bit words, if ``pad`` is not :obj:`bytes`,
1590+
or if the option data would exceed the 255 octets
1591+
``Opt Data Len`` can count.
15871592
15881593
Note:
15891594
``Cmpt Length`` on the wire counts 32-bit words [:rfc:`5570#section-5.1`],
@@ -1596,10 +1601,26 @@ def _make_opt_calipso(self, code: 'Enum_Option', opt: 'Optional[Data_CALIPSOOpti
15961601
checksum = opt.checksum
15971602
bitmap = opt.cmpt_bitmap if hasattr(opt, 'cmpt_bitmap') else None
15981603
pad = opt.pad
1599-
bitmap_bytes = bytes(bitmap) if bitmap is not None else b''
1604+
if bitmap is None:
1605+
bitmap_bytes = b''
1606+
elif isinstance(bitmap, (bytes, bytearray, tuple, list)):
1607+
try:
1608+
bitmap_bytes = bytes(bitmap)
1609+
except (TypeError, ValueError) as exc:
1610+
raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid CALIPSO bitmap: '
1611+
f'{exc}') from exc
1612+
else:
1613+
raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid CALIPSO bitmap type: '
1614+
f'{type(bitmap).__name__}')
16001615
if len(bitmap_bytes) % 4 != 0:
16011616
raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid CALIPSO bitmap length: '
16021617
f'{len(bitmap_bytes)}')
1618+
if not isinstance(pad, (bytes, bytearray)):
1619+
raise ProtocolError(f'{self.alias}: [OptNo {code}] invalid CALIPSO pad type: '
1620+
f'{type(pad).__name__}')
1621+
if 8 + len(bitmap_bytes) + len(pad) > 255:
1622+
raise ProtocolError(f'{self.alias}: [OptNo {code}] too long CALIPSO option data: '
1623+
f'{8 + len(bitmap_bytes) + len(pad)}')
16031624

16041625
return Schema_CALIPSOOption(
16051626
type=code,
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# -*- coding: utf-8 -*-
2+
"""Constructing a CALIPSO option validates its data length and its pad.
3+
4+
GitHub issue #1408: ``Opt Data Len`` is one octet, but ``make()`` wrote it
5+
unchecked, so a bitmap plus pad over 255 octets produced a corrupt length
6+
octet. A pad that was not :obj:`bytes`, or a bitmap that was not a sequence
7+
of octets, surfaced as a raw :exc:`struct.error`, :exc:`TypeError` or
8+
:exc:`ValueError`. All of these now raise
9+
:exc:`~pcapkit.utilities.exceptions.ProtocolError`. ``make()`` also built an
10+
odd ``Cmpt Length``, which RFC 5570 permits, but the reader rejected it; the
11+
reader now accepts it.
12+
13+
The classes are imported inside each test, after
14+
:func:`~tests._support.reimport_once_per_class`, so that they belong to the
15+
:mod:`pcapkit` import that is live when the test runs.
16+
17+
"""
18+
19+
import importlib
20+
import io
21+
import unittest
22+
23+
from tests._support import reimport_once_per_class
24+
25+
#: The protocol classes sharing the option code, by module and class name.
26+
PROTOCOLS = (
27+
('pcapkit.protocols.internet.hopopt', 'HOPOPT'),
28+
('pcapkit.protocols.internet.ipv6_opts', 'IPv6_Opts'),
29+
)
30+
31+
32+
class TestCALIPSOMakeValidation(unittest.TestCase):
33+
"""Pin the CALIPSO ``make()`` length and pad checks."""
34+
35+
def setUp(self) -> None:
36+
reimport_once_per_class(self)
37+
38+
def _protocols(self) -> 'list[type]':
39+
return [getattr(importlib.import_module(mod), name) for mod, name in PROTOCOLS]
40+
41+
def _make(self, cls: 'type', **fields: 'object') -> 'object':
42+
from pcapkit.const.ipv6.option import Option
43+
44+
return cls(next=59, options=[(Option.CALIPSO, dict(domain=1, level=5, **fields))])
45+
46+
def test_overlong_option_data_raises(self) -> None:
47+
from pcapkit.utilities.exceptions import ProtocolError
48+
49+
cases = {
50+
'pad 248': {'pad': b'\x00' * 248},
51+
'pad 1000': {'pad': b'\x00' * 1000},
52+
'bitmap 248': {'bitmap': b'\x00' * 248},
53+
'bitmap 240 + pad 8': {'bitmap': b'\x00' * 240, 'pad': b'\x00' * 8},
54+
}
55+
for cls in self._protocols():
56+
for label, fields in cases.items():
57+
with self.subTest(protocol=cls.__name__, case=label):
58+
with self.assertRaisesRegex(ProtocolError, 'too long CALIPSO option data'):
59+
self._make(cls, **fields)
60+
61+
def test_largest_option_data_round_trips(self) -> None:
62+
from pcapkit.const.ipv6.option import Option
63+
64+
for cls in self._protocols():
65+
for label, fields in {
66+
'pad 247': {'pad': b'\xaa' * 247},
67+
'bitmap 240 + pad 7': {'bitmap': b'\x11' * 240, 'pad': b'\xaa' * 7},
68+
}.items():
69+
with self.subTest(protocol=cls.__name__, case=label):
70+
made = self._make(cls, **fields)
71+
data = made.data # type: ignore[attr-defined]
72+
self.assertEqual(data[2:4], b'\x07\xff')
73+
parsed = cls(io.BytesIO(data), len(data), extension=True)
74+
self.assertEqual(parsed.info.options[Option.CALIPSO].pad, fields['pad']) # type: ignore[attr-defined]
75+
self.assertEqual(cls.from_data(parsed.info).data, data)
76+
77+
def test_non_bytes_pad_raises(self) -> None:
78+
from pcapkit.utilities.exceptions import ProtocolError
79+
80+
for cls in self._protocols():
81+
for pad in ('abcd', [0, 0, 0, 0], None, 4):
82+
with self.subTest(protocol=cls.__name__, pad=pad):
83+
with self.assertRaisesRegex(ProtocolError, 'invalid CALIPSO pad type'):
84+
self._make(cls, pad=pad)
85+
86+
def test_bad_bitmap_raises(self) -> None:
87+
from pcapkit.utilities.exceptions import ProtocolError
88+
89+
cases = {
90+
'element over 255': ((256, 0, 0, 0), 'invalid CALIPSO bitmap: '),
91+
'negative element': ((-1, 0, 0, 0), 'invalid CALIPSO bitmap: '),
92+
'str element': (('a', 0, 0, 0), 'invalid CALIPSO bitmap: '),
93+
'str bitmap': ('abcd', 'invalid CALIPSO bitmap type'),
94+
'int bitmap': (4, 'invalid CALIPSO bitmap type'),
95+
}
96+
for cls in self._protocols():
97+
for label, (bitmap, message) in cases.items():
98+
with self.subTest(protocol=cls.__name__, case=label):
99+
with self.assertRaisesRegex(ProtocolError, message):
100+
self._make(cls, bitmap=bitmap)
101+
102+
def test_odd_cmpt_length_round_trips(self) -> None:
103+
# RFC 5570, section 5.2: the bitmap is sized in 32-bit words, so an
104+
# odd ``Cmpt Length`` (here 1 and 3) is valid and has to re-parse
105+
from pcapkit.const.ipv6.option import Option
106+
107+
for cls in self._protocols():
108+
for words in (1, 3):
109+
bitmap = bytes(range(1, 4 * words + 1))
110+
with self.subTest(protocol=cls.__name__, cmpt_len=words):
111+
data = self._make(cls, bitmap=bitmap).data # type: ignore[attr-defined]
112+
self.assertEqual(data[4 + 4], words)
113+
parsed = cls(io.BytesIO(data), len(data), extension=True)
114+
calipso = parsed.info.options[Option.CALIPSO] # type: ignore[attr-defined]
115+
self.assertEqual(calipso.cmpt_len, 4 * words)
116+
self.assertEqual(bytes(calipso.cmpt_bitmap), bitmap)
117+
self.assertEqual(cls.from_data(parsed.info).data, data)
118+
119+
def test_bitmap_sequences_match_bytes(self) -> None:
120+
for cls in self._protocols():
121+
expected = self._make(cls, bitmap=b'\x01\x02\x03\x04\x05\x06\x07\x08').data # type: ignore[attr-defined]
122+
for bitmap in (bytearray(range(1, 9)), tuple(range(1, 9)), list(range(1, 9))):
123+
with self.subTest(protocol=cls.__name__, bitmap=type(bitmap).__name__):
124+
self.assertEqual(self._make(cls, bitmap=bitmap).data, expected) # type: ignore[attr-defined]
125+
126+
def test_bytearray_pad_matches_bytes(self) -> None:
127+
for cls in self._protocols():
128+
with self.subTest(protocol=cls.__name__):
129+
self.assertEqual(self._make(cls, pad=bytearray(b'\xaa\xbb\xcc\xdd')).data, # type: ignore[attr-defined]
130+
self._make(cls, pad=b'\xaa\xbb\xcc\xdd').data) # type: ignore[attr-defined]
131+
132+
133+
if __name__ == '__main__':
134+
unittest.main()

0 commit comments

Comments
 (0)