diff --git a/docs/source/pcapkit/protocols/internet/hip.rst b/docs/source/pcapkit/protocols/internet/hip.rst index cbb1b3ef5e..b3bceb9f19 100644 --- a/docs/source/pcapkit/protocols/internet/hip.rst +++ b/docs/source/pcapkit/protocols/internet/hip.rst @@ -425,6 +425,7 @@ Auxiliary Functions .. autofunction:: pcapkit.protocols.schema.internet.hip.locator_value_selector .. autofunction:: pcapkit.protocols.schema.internet.hip.host_id_hi_selector .. autofunction:: pcapkit.protocols.schema.internet.hip.registration_type_list_len +.. autofunction:: pcapkit.protocols.schema.internet.hip.reg_info_list_len Data Models ----------- diff --git a/docs/source/pcapkit/protocols/internet/hopopt.rst b/docs/source/pcapkit/protocols/internet/hopopt.rst index bd9c9a2a0e..3537a9cacc 100644 --- a/docs/source/pcapkit/protocols/internet/hopopt.rst +++ b/docs/source/pcapkit/protocols/internet/hopopt.rst @@ -219,6 +219,8 @@ Auxiliary Functions .. autofunction:: pcapkit.protocols.schema.internet.hopopt.mpl_opt_seed_id_len .. autofunction:: pcapkit.protocols.schema.internet.hopopt.pad_opt_data_len +.. autofunction:: pcapkit.protocols.schema.internet.hopopt.calipso_pad_len +.. autofunction:: pcapkit.protocols.schema.internet.hopopt.mpl_opt_pad_len .. autofunction:: pcapkit.protocols.schema.internet.hopopt.smf_dpd_data_selector .. autofunction:: pcapkit.protocols.schema.internet.hopopt.smf_i_dpd_tid_selector diff --git a/docs/source/pcapkit/protocols/internet/ipv6_opts.rst b/docs/source/pcapkit/protocols/internet/ipv6_opts.rst index e7c6abb153..89d6bc7768 100644 --- a/docs/source/pcapkit/protocols/internet/ipv6_opts.rst +++ b/docs/source/pcapkit/protocols/internet/ipv6_opts.rst @@ -219,6 +219,8 @@ Auxiliary Functions .. autofunction:: pcapkit.protocols.schema.internet.ipv6_opts.mpl_opt_seed_id_len .. autofunction:: pcapkit.protocols.schema.internet.ipv6_opts.pad_opt_data_len +.. autofunction:: pcapkit.protocols.schema.internet.ipv6_opts.calipso_pad_len +.. autofunction:: pcapkit.protocols.schema.internet.ipv6_opts.mpl_opt_pad_len .. autofunction:: pcapkit.protocols.schema.internet.ipv6_opts.smf_dpd_data_selector .. autofunction:: pcapkit.protocols.schema.internet.ipv6_opts.smf_i_dpd_tid_selector diff --git a/pcapkit/protocols/schema/internet/hip.py b/pcapkit/protocols/schema/internet/hip.py index 7f9f1c617d..4bedd87001 100644 --- a/pcapkit/protocols/schema/internet/hip.py +++ b/pcapkit/protocols/schema/internet/hip.py @@ -190,6 +190,32 @@ def registration_type_list_len(pkt: 'dict[str, Any]') -> 'int': return length +def reg_info_list_len(pkt: 'dict[str, Any]') -> 'int': + """Return ``REG_INFO`` registration type list length. + + Used by the ``reg_info`` field of :class:`RegInfoParameter`, which follows + a pair of ``min_lifetime`` and ``max_lifetime`` octets with a list of + registration type octets sized by the remainder of the parameter. + + Args: + pkt: Parameter unpacked schema. + + Returns: + Registration type list length. + + Raises: + FieldValueError: If the parameter's ``Length`` on the wire is too + short to hold the ``min_lifetime`` and ``max_lifetime`` octets + already read, which would otherwise underflow the list length + below zero. + + """ + length = pkt['len'] - 2 + if length < 0: + raise FieldValueError(f'HIP: invalid parameter length: {pkt["len"]}') + return length + + class Parameter(EnumSchema[Enum_Parameter]): """Base schema for HIP parameters.""" @@ -704,7 +730,7 @@ class RegInfoParameter(Parameter, code=Enum_Parameter.REG_INFO): max_lifetime: 'int' = UInt8Field() #: Registration types. reg_info: 'list[Enum_Registration]' = ListField( - length=lambda pkt: pkt['len'] - 2, + length=reg_info_list_len, item_type=EnumField(length=1, namespace=Enum_Registration), ) #: Padding. diff --git a/pcapkit/protocols/schema/internet/hopopt.py b/pcapkit/protocols/schema/internet/hopopt.py index c74527d68b..1b7553cc9a 100644 --- a/pcapkit/protocols/schema/internet/hopopt.py +++ b/pcapkit/protocols/schema/internet/hopopt.py @@ -288,6 +288,49 @@ def pad_opt_data_len(pkt: 'dict[str, Any]') -> 'int': return length +def calipso_pad_len(pkt: 'dict[str, Any]') -> 'int': + """Return CALIPSO option padding length. + + Args: + pkt: CALIPSO option unpacked schema. + + Returns: + CALIPSO option padding length. + + Raises: + FieldValueError: If ``Opt Data Len`` on the wire is too short to hold + the fixed header and the compartment bitmap declared by + ``cmpt_len``, which would otherwise underflow the padding length + below zero. + + """ + length = pkt['len'] - 8 - pkt['cmpt_len'] * 4 + if length < 0: + raise FieldValueError(f'HOPOPT: invalid CALIPSO option length: {pkt["len"]}') + return length + + +def mpl_opt_pad_len(pkt: 'dict[str, Any]') -> 'int': + """Return MPL option padding length. + + Args: + pkt: MPL option unpacked schema. + + Returns: + MPL option padding length. + + Raises: + FieldValueError: If ``Opt Data Len`` on the wire is too short to hold + the fixed header and the Seed-ID declared by ``flags.type``, which + would otherwise underflow the padding length below zero. + + """ + length = pkt['len'] - 2 - (0 if pkt['flags']['type'] == 0 else mpl_opt_seed_id_len(pkt)) + if length < 0: + raise FieldValueError(f'HOPOPT: invalid MPL option length: {pkt["len"]}') + return length + + class Option(EnumSchema[Enum_Option]): """Header schema for HOPOPT options.""" @@ -389,7 +432,7 @@ class CALIPSOOption(Option, code=Enum_Option.CALIPSO): lambda pkt: pkt['cmpt_len'] > 0, ) #: Padding. - pad: 'bytes' = PaddingField(length=lambda pkt: pkt['len'] - 8 - pkt['cmpt_len'] * 4) + pad: 'bytes' = PaddingField(length=calipso_pad_len) if TYPE_CHECKING: def __init__(self, type: 'Enum_Option', len: 'int', domain: 'int', cmpt_len: 'int', @@ -650,9 +693,7 @@ class MPLOption(Option, code=Enum_Option.MPL_Option): lambda pkt: pkt['flags']['type'] != Enum_SeedID.IPV6_SOURCE_ADDRESS, ) #: Reserved data (padding). - pad: 'bytes' = PaddingField(length=lambda pkt: pkt['len'] - 2 - ( - 0 if pkt['flags']['type'] == 0 else mpl_opt_seed_id_len(pkt) - )) + pad: 'bytes' = PaddingField(length=mpl_opt_pad_len) def post_process(self, packet: 'dict[str, Any]') -> 'Schema': """Revise ``schema`` data after unpacking process. diff --git a/pcapkit/protocols/schema/internet/ipv6_opts.py b/pcapkit/protocols/schema/internet/ipv6_opts.py index 894dc12f57..1226f3c6fe 100644 --- a/pcapkit/protocols/schema/internet/ipv6_opts.py +++ b/pcapkit/protocols/schema/internet/ipv6_opts.py @@ -288,6 +288,49 @@ def pad_opt_data_len(pkt: 'dict[str, Any]') -> 'int': return length +def calipso_pad_len(pkt: 'dict[str, Any]') -> 'int': + """Return CALIPSO option padding length. + + Args: + pkt: CALIPSO option unpacked schema. + + Returns: + CALIPSO option padding length. + + Raises: + FieldValueError: If ``Opt Data Len`` on the wire is too short to hold + the fixed header and the compartment bitmap declared by + ``cmpt_len``, which would otherwise underflow the padding length + below zero. + + """ + length = pkt['len'] - 8 - pkt['cmpt_len'] * 4 + if length < 0: + raise FieldValueError(f'IPv6-Opts: invalid CALIPSO option length: {pkt["len"]}') + return length + + +def mpl_opt_pad_len(pkt: 'dict[str, Any]') -> 'int': + """Return MPL option padding length. + + Args: + pkt: MPL option unpacked schema. + + Returns: + MPL option padding length. + + Raises: + FieldValueError: If ``Opt Data Len`` on the wire is too short to hold + the fixed header and the Seed-ID declared by ``flags.type``, which + would otherwise underflow the padding length below zero. + + """ + length = pkt['len'] - 2 - (0 if pkt['flags']['type'] == 0 else mpl_opt_seed_id_len(pkt)) + if length < 0: + raise FieldValueError(f'IPv6-Opts: invalid MPL option length: {pkt["len"]}') + return length + + class Option(EnumSchema[Enum_Option]): """Header schema for IPv6-Opts options.""" @@ -389,7 +432,7 @@ class CALIPSOOption(Option, code=Enum_Option.CALIPSO): lambda pkt: pkt['cmpt_len'] > 0, ) #: Padding. - pad: 'bytes' = PaddingField(length=lambda pkt: pkt['len'] - 8 - pkt['cmpt_len'] * 4) + pad: 'bytes' = PaddingField(length=calipso_pad_len) if TYPE_CHECKING: def __init__(self, type: 'Enum_Option', len: 'int', domain: 'int', cmpt_len: 'int', @@ -655,9 +698,7 @@ class MPLOption(Option, code=Enum_Option.MPL_Option): lambda pkt: pkt['flags']['type'] != Enum_SeedID.IPV6_SOURCE_ADDRESS, ) #: Reserved data (padding). - pad: 'bytes' = PaddingField(length=lambda pkt: pkt['len'] - 2 - ( - 0 if pkt['flags']['type'] == 0 else mpl_opt_seed_id_len(pkt) - )) + pad: 'bytes' = PaddingField(length=mpl_opt_pad_len) def post_process(self, packet: 'dict[str, Any]') -> 'Schema': """Revise ``schema`` data after unpacking process. diff --git a/tests/protocols/internet/test_hip_unit.py b/tests/protocols/internet/test_hip_unit.py index aa0fc541c2..2ae772b3d4 100644 --- a/tests/protocols/internet/test_hip_unit.py +++ b/tests/protocols/internet/test_hip_unit.py @@ -1801,6 +1801,45 @@ def test_hip_registration_parameters_reject_underflowing_length(self) -> None: with self.assertRaisesRegex(FieldValueError, 'invalid parameter length'): HIP(raw, len(raw), extension=True) + def test_hip_reg_info_parameter_rejects_underflowing_length(self) -> None: + """#455: a ``REG_INFO`` parameter's ``Length`` too small for its own + ``min_lifetime``/``max_lifetime`` octets must raise, not silently + drop the registration list. + + ``reg_info`` sizes its list of registration-type octets as + ``Length - 2`` -- the two octets are ``min_lifetime`` and + ``max_lifetime``, which ``REG_INFO`` carries in place of the single + ``lifetime`` octet #438 fixed for ``reg_request``/``reg_response``/ + ``reg_failed``. Nothing floored that at zero either, so a peer + declaring ``Length = 0`` drove the list length to ``-2``. Unlike a + :class:`~pcapkit.corekit.fields.strings.BytesField`, + :class:`~pcapkit.corekit.fields.collections.ListField` never reaches + :func:`struct.calcsize` for a negative length -- its own ``while + length > 0`` loop just returns an empty list instead -- so this + parsed to an empty ``reg_type`` with no exception and no diagnostic: + ``proto.info.parameters[930].reg_type == ()``, confirmed against the + pre-fix tree at ``da2422728``, rather than rejecting the malformed + ``Length``. + + """ + from pcapkit.protocols.internet.hip import HIP + from pcapkit.utilities.exceptions import FieldValueError + + # next(1) len(1)=5 pkt(1) ver(1)=0x01 (the reserved bit that must be 1) + # checksum(2) control(2) shit(16) rhit(16) -- the fixed 40-octet header, + # declaring one 8-octet parameter to follow: (5 - 4) * 8 == 8. + fixed = bytes([0x3b, 0x05, 0x00, 0x01]) + bytes(2) + bytes(2) + bytes(16) + bytes(16) + self.assertEqual(len(fixed), 40) + + # type(2)=930 (REG_INFO) len(2)=0, min_lifetime(1) max_lifetime(1), + # then 2 octets padding out the 8-octet parameter area the outer + # header declared. + param = (930).to_bytes(2, 'big') + (0).to_bytes(2, 'big') + bytes(2) + bytes(2) + raw = fixed + param + + with self.assertRaisesRegex(FieldValueError, 'invalid parameter length'): + HIP(raw, len(raw), extension=True) + def test_hip_schema_selectors_and_encrypted_parameter_branches(self) -> None: from pcapkit.const.hip.cipher import Cipher from pcapkit.const.hip.hi_algorithm import HIAlgorithm diff --git a/tests/protocols/internet/test_ipv6_extension_unit.py b/tests/protocols/internet/test_ipv6_extension_unit.py index 231601fd38..0c8c5686a3 100644 --- a/tests/protocols/internet/test_ipv6_extension_unit.py +++ b/tests/protocols/internet/test_ipv6_extension_unit.py @@ -1726,6 +1726,77 @@ def test_ipv6_opts_identification_based_dpd_option_rejects_underflowing_length(s self._assert_identification_based_dpd_option_rejects_underflowing_length(IPv6_Opts) + def _assert_calipso_option_rejects_underflowing_length(self, protocol_cls: type) -> None: + """#455: ``Opt Data Len`` too small for CALIPSO's own fixed fields must raise, not crash. + + This is the issue's own reproduction: a CALIPSO option declaring + ``Opt Data Len = 0`` and ``Cmpt Len = 0``. ``pad``'s length is + ``Opt Data Len - 8 - Cmpt Len * 4`` -- the octets left over after the + option's own ``domain``, ``cmpt_len``, ``level`` and ``checksum`` + fields (8 octets fixed) and its compartment bitmap (``Cmpt Len * 4`` + octets) -- and nothing floored that at zero, so it drove the padding + length to ``-8``. That reached :func:`struct.calcsize` as the template + ``'-8s'`` and raised a bare ``struct.error: bad char in struct + format`` -- not one of pcapkit's own exception types, and uncatchable + through :mod:`pcapkit.utilities.exceptions`. + + """ + from pcapkit.utilities.exceptions import FieldValueError + + # next(1) hdr_ext_len(1)=1 -> 16-octet extension header; option + # type(1)=CALIPSO(0x07) len(1)=0, then domain(4) cmpt_len(1) level(1) + # checksum(2) all zero, with four trailing zero octets. + raw = bytes.fromhex('3b0007000000000000000000000000') + + with self.assertRaisesRegex(FieldValueError, 'invalid CALIPSO option length'): + with time_limit(5): + protocol_cls(raw, extension=True) + + def test_hopopt_calipso_option_rejects_underflowing_length(self) -> None: + from pcapkit.protocols.internet.hopopt import HOPOPT + + self._assert_calipso_option_rejects_underflowing_length(HOPOPT) + + def test_ipv6_opts_calipso_option_rejects_underflowing_length(self) -> None: + from pcapkit.protocols.internet.ipv6_opts import IPv6_Opts + + self._assert_calipso_option_rejects_underflowing_length(IPv6_Opts) + + def _assert_mpl_option_rejects_underflowing_length(self, protocol_cls: type) -> None: + """#455: ``Opt Data Len`` too small for MPL's own ``flags``/``seq`` octets must raise, not crash. + + An MPL option declaring ``Opt Data Len = 0`` with Seed-ID type ``0`` + (no Seed-ID octets). ``pad``'s length is ``Opt Data Len - 2 - + `` -- the two octets are the ``flags`` and ``seq`` + fields the option always carries -- and nothing floored that at zero, + so it drove the padding length to ``-2``. That reached + :func:`struct.calcsize` as the template ``'-2s'`` and raised a bare + ``struct.error: bad char in struct format`` -- not one of pcapkit's + own exception types, and uncatchable through + :mod:`pcapkit.utilities.exceptions`. + + """ + from pcapkit.utilities.exceptions import FieldValueError + + # next(1) hdr_ext_len(1)=0 -> 8-octet extension header; option + # type(1)=MPL_Option(0x6d) len(1)=0, flags(1)=0 (Seed-ID type 0), + # seq(1)=0, with two trailing zero octets. + raw = bytes.fromhex('3b006d0000000000') + + with self.assertRaisesRegex(FieldValueError, 'invalid MPL option length'): + with time_limit(5): + protocol_cls(raw, extension=True) + + def test_hopopt_mpl_option_rejects_underflowing_length(self) -> None: + from pcapkit.protocols.internet.hopopt import HOPOPT + + self._assert_mpl_option_rejects_underflowing_length(HOPOPT) + + def test_ipv6_opts_mpl_option_rejects_underflowing_length(self) -> None: + from pcapkit.protocols.internet.ipv6_opts import IPv6_Opts + + self._assert_mpl_option_rejects_underflowing_length(IPv6_Opts) + def _assert_a_truncated_option_area_is_diagnosed(self, protocol_cls: type) -> None: """An option area with nothing behind it is an error, not a hang.