|
| 1 | +# -*- coding: utf-8 -*- |
| 2 | +"""An option declaring more octets than are present is rejected, not zero-filled. |
| 3 | +
|
| 4 | +GitHub issue #1325: :meth:`OptionField.unpack |
| 5 | +<pcapkit.corekit.fields.collections.OptionField.unpack>` let an option schema |
| 6 | +read past the end of its data, and every field reading off the end padded the |
| 7 | +missing octets with zeros. A HIP ``ACK`` declaring 256 octets with 4 present |
| 8 | +parsed as 64 update IDs and rebuilt to 304 octets; an MH ``PadN`` declaring 3 |
| 9 | +octets with none present rebuilt 24 octets from 16. |
| 10 | +
|
| 11 | +Such an option now raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`. |
| 12 | +Inside a parent layer the octets are kept as captured -- raw extension data |
| 13 | +under IPv6, a :class:`~pcapkit.protocols.misc.raw.Raw` payload elsewhere -- and |
| 14 | +the parent rebuilds byte for byte. |
| 15 | +
|
| 16 | +An option that begins at the end of the data reads nothing at all, and is still |
| 17 | +left to the end-of-option-list handling an over-long option area relies on. |
| 18 | +
|
| 19 | +Every case builds its own octets in memory and reads no capture. |
| 20 | +
|
| 21 | +""" |
| 22 | + |
| 23 | +import struct |
| 24 | +import unittest |
| 25 | +import warnings |
| 26 | + |
| 27 | +from tests._support import reimport_once_per_class |
| 28 | + |
| 29 | + |
| 30 | +def _hip(params: bytes) -> bytes: |
| 31 | + """A HIP ``I1`` header (40 octets) carrying ``params``.""" |
| 32 | + return (bytes([59, (40 + len(params)) // 8 - 1, 1, 0x11, 0, 0, 0, 0]) |
| 33 | + + b'\x11' * 16 + b'\x22' * 16 + params) |
| 34 | + |
| 35 | + |
| 36 | +def _ext6(opts: bytes) -> bytes: |
| 37 | + """An IPv6 options extension header carrying ``opts``.""" |
| 38 | + return bytes([59, (2 + len(opts)) // 8 - 1]) + opts |
| 39 | + |
| 40 | + |
| 41 | +def _sctp(chunks: bytes) -> bytes: |
| 42 | + """An SCTP common header carrying ``chunks``.""" |
| 43 | + return struct.pack('>HHII', 1, 2, 0, 0) + chunks |
| 44 | + |
| 45 | + |
| 46 | +def _tcp(opts: bytes) -> bytes: |
| 47 | + """A TCP header carrying ``opts``.""" |
| 48 | + return struct.pack('>HHIIBBHHH', 1, 2, 0, 0, (5 + len(opts) // 4) << 4, 0x10, 0, 0, 0) + opts |
| 49 | + |
| 50 | + |
| 51 | +def _ipv4(opts: bytes, ihl: int, proto: int = 253, payload: bytes = b'') -> bytes: |
| 52 | + """An IPv4 header declaring ``ihl`` and carrying ``opts`` and ``payload``.""" |
| 53 | + return (bytes([0x40 | ihl, 0]) + struct.pack('>H', 20 + len(opts) + len(payload)) |
| 54 | + + bytes.fromhex('00010000 40') + bytes([proto]) + bytes(2) |
| 55 | + + bytes.fromhex('0a000001 0a000002') + opts + payload) |
| 56 | + |
| 57 | + |
| 58 | +def _ipv6(proto: int, payload: bytes) -> bytes: |
| 59 | + """An IPv6 header carrying ``payload`` as next header ``proto``.""" |
| 60 | + return (bytes.fromhex('60000000') + struct.pack('>H', len(payload)) + bytes([proto, 64]) |
| 61 | + + b'\x11' * 16 + b'\x22' * 16 + payload) |
| 62 | + |
| 63 | + |
| 64 | +#: Option areas whose last option declares more octets than the area holds. |
| 65 | +OVERRUNS = { |
| 66 | + # issue #1325: ACK declaring 0x0100 octets, 4 present |
| 67 | + 'HIP ACK': ('pcapkit.protocols.internet.hip', 'HIP', _hip(bytes.fromhex('01c10100 00000000'))), |
| 68 | + # issue #1325: PadN declaring 3 octets, none present |
| 69 | + 'MH PadN': ('pcapkit.protocols.internet.mh', 'MH', |
| 70 | + bytes.fromhex('3b010000 00000000 010400000000 0103')), |
| 71 | + 'HOPOPT PadN': ('pcapkit.protocols.internet.hopopt', 'HOPOPT', _ext6(bytes.fromhex('010700000000'))), |
| 72 | + 'IPv6-Opts PadN': ('pcapkit.protocols.internet.ipv6_opts', 'IPv6_Opts', _ext6(bytes.fromhex('010700000000'))), |
| 73 | + 'SCTP INIT parameter': ('pcapkit.protocols.transport.sctp', 'SCTP', _sctp( |
| 74 | + bytes.fromhex('01000020 00000001 00010000 00010001 00000001 000c0010 00050000'))), |
| 75 | + # an unassigned kind declaring 12 octets, 8 present |
| 76 | + 'TCP unassigned': ('pcapkit.protocols.transport.tcp', 'TCP', _tcp(bytes.fromhex('4f0caabb ccddeeff'))), |
| 77 | + # an unassigned option declaring 12 octets, 8 present in a 16-octet area |
| 78 | + 'IPv4 unassigned': ('pcapkit.protocols.internet.ipv4', 'IPv4', |
| 79 | + _ipv4(bytes.fromhex('1f0caabb ccddeeff'), ihl=9)), |
| 80 | +} |
| 81 | + |
| 82 | +#: The same option areas with every option complete. |
| 83 | +CONTROLS = { |
| 84 | + 'HIP ACK': ('pcapkit.protocols.internet.hip', 'HIP', _hip(bytes.fromhex('01c10004 00000001'))), |
| 85 | + 'MH PadN': ('pcapkit.protocols.internet.mh', 'MH', |
| 86 | + bytes.fromhex('3b010000 00000000 0106000000000000')), |
| 87 | + 'HOPOPT PadN': ('pcapkit.protocols.internet.hopopt', 'HOPOPT', _ext6(bytes.fromhex('010400000000'))), |
| 88 | + 'IPv6-Opts PadN': ('pcapkit.protocols.internet.ipv6_opts', 'IPv6_Opts', _ext6(bytes.fromhex('010400000000'))), |
| 89 | + 'SCTP INIT parameter': ('pcapkit.protocols.transport.sctp', 'SCTP', _sctp( |
| 90 | + bytes.fromhex('0100001c 00000001 00010000 00010001 00000001 000c0008 00050006'))), |
| 91 | + 'TCP unassigned': ('pcapkit.protocols.transport.tcp', 'TCP', _tcp(bytes.fromhex('4f08aabb ccddeeff'))), |
| 92 | + 'IPv4 unassigned': ('pcapkit.protocols.internet.ipv4', 'IPv4', |
| 93 | + _ipv4(bytes.fromhex('1f08aabb ccddeeff'), ihl=7)), |
| 94 | +} |
| 95 | + |
| 96 | +#: Each overrun inside a parent layer: (parent module, parent class, frame). |
| 97 | +IN_PARENT = { |
| 98 | + 'HIP ACK': ('pcapkit.protocols.internet.ipv6', 'IPv6', _ipv6(139, OVERRUNS['HIP ACK'][2])), |
| 99 | + 'MH PadN': ('pcapkit.protocols.internet.ipv6', 'IPv6', _ipv6(135, OVERRUNS['MH PadN'][2])), |
| 100 | + 'HOPOPT PadN': ('pcapkit.protocols.internet.ipv6', 'IPv6', _ipv6(0, OVERRUNS['HOPOPT PadN'][2])), |
| 101 | + 'IPv6-Opts PadN': ('pcapkit.protocols.internet.ipv6', 'IPv6', _ipv6(60, OVERRUNS['IPv6-Opts PadN'][2])), |
| 102 | + 'SCTP INIT parameter': ('pcapkit.protocols.internet.ipv4', 'IPv4', |
| 103 | + _ipv4(b'', ihl=5, proto=132, payload=OVERRUNS['SCTP INIT parameter'][2])), |
| 104 | + 'TCP unassigned': ('pcapkit.protocols.internet.ipv4', 'IPv4', |
| 105 | + _ipv4(b'', ihl=5, proto=6, payload=OVERRUNS['TCP unassigned'][2])), |
| 106 | + 'IPv4 unassigned': ('pcapkit.protocols.link.ethernet', 'Ethernet', |
| 107 | + bytes.fromhex('0123456789ab fedcba987654 0800') + OVERRUNS['IPv4 unassigned'][2]), |
| 108 | +} |
| 109 | + |
| 110 | + |
| 111 | +class TestDeclaredLengthOverrun(unittest.TestCase): |
| 112 | + """Pin the rejection of an option running past the end of its data.""" |
| 113 | + |
| 114 | + def setUp(self) -> None: |
| 115 | + reimport_once_per_class(self) |
| 116 | + |
| 117 | + @staticmethod |
| 118 | + def _protocol(module: str, name: str) -> type: |
| 119 | + import importlib |
| 120 | + |
| 121 | + return getattr(importlib.import_module(module), name) |
| 122 | + |
| 123 | + def test_an_overrunning_option_raises_protocol_error(self) -> None: |
| 124 | + from pcapkit.utilities.exceptions import ProtocolError |
| 125 | + |
| 126 | + for case, (module, name, data) in OVERRUNS.items(): |
| 127 | + with self.subTest(case=case): |
| 128 | + protocol = self._protocol(module, name) |
| 129 | + with warnings.catch_warnings(): |
| 130 | + warnings.simplefilter('ignore') |
| 131 | + with self.assertRaisesRegex(ProtocolError, 'runs past the end of the data'): |
| 132 | + protocol(data, len(data)) |
| 133 | + |
| 134 | + def test_a_complete_option_still_rebuilds_byte_for_byte(self) -> None: |
| 135 | + for case, (module, name, data) in CONTROLS.items(): |
| 136 | + with self.subTest(case=case): |
| 137 | + protocol = self._protocol(module, name) |
| 138 | + parsed = protocol(data, len(data)) |
| 139 | + self.assertEqual(protocol.from_data(parsed.info).data, data) |
| 140 | + |
| 141 | + def test_an_overrunning_layer_is_kept_as_captured_by_its_parent(self) -> None: |
| 142 | + from pcapkit.protocols.misc.raw import Raw |
| 143 | + |
| 144 | + for case, (module, name, frame) in IN_PARENT.items(): |
| 145 | + with self.subTest(case=case): |
| 146 | + protocol = self._protocol(module, name) |
| 147 | + with warnings.catch_warnings(): |
| 148 | + warnings.simplefilter('ignore') |
| 149 | + parsed = protocol(frame, len(frame)) |
| 150 | + |
| 151 | + if name == 'IPv6': |
| 152 | + # an extension header is kept as raw extension data |
| 153 | + self.assertIn('runs past the end of the data', str(parsed.info.ext.error)) |
| 154 | + else: |
| 155 | + # any other layer is kept as a raw payload, as captured |
| 156 | + self.assertIsInstance(parsed.payload, Raw) |
| 157 | + self.assertIn('runs past the end of the data', str(parsed.payload.info.error)) |
| 158 | + self.assertEqual(parsed.payload.data, OVERRUNS[case][2]) |
| 159 | + self.assertEqual(protocol.from_data(parsed.info).data, frame) |
| 160 | + |
| 161 | + def test_an_option_area_declared_past_the_data_still_ends_at_eool(self) -> None: |
| 162 | + from pcapkit.protocols.internet.ipv4 import IPv4 |
| 163 | + |
| 164 | + # ``ihl`` declares 20 octets of options with none present: the first |
| 165 | + # option begins at the end of the data, so it is end-of-option-list. |
| 166 | + data = bytes.fromhex('4a00001800010000400600000a0000010a000002') |
| 167 | + with warnings.catch_warnings(): |
| 168 | + warnings.simplefilter('ignore') |
| 169 | + parsed = IPv4(data, len(data)) |
| 170 | + self.assertEqual(parsed.info.src.compressed, '10.0.0.1') |
| 171 | + |
| 172 | + |
| 173 | +if __name__ == '__main__': |
| 174 | + unittest.main() |
0 commit comments