mh: reject a wrong-type MN-ID identifier in-library, not as a stdlib leak - #481
Conversation
…leak _make_opt_mn_id documents identifier as bytes | str | IPv6Address | int, but each subtype's field (mn_id_selector) accepts only one of the first two: a StringField for NAI, a BytesField for the other six. Handing the wrong one -- or a value of neither type -- escaped as a bare stdlib exception instead of a ProtocolError. #468 already closed the int half of this (#467); this closes the rest (#469). Measured through the maker on e80c422, all now raising ProtocolError instead of leaking: - NAI + bytes/list/dict -> was AttributeError ('bytes' object has no attribute 'encode') - NAI + float/None/IPv6Address -> was TypeError (no len()) - IMSI/P_TMSI/EUI_48_address/EUI_64_address/GUTI/DUID + str/list/dict -> was struct.error (argument for 's' must be a bytes object) - same six + float/None/IPv6Address -> was TypeError (no len()) - IPv6_Address + any of the above -> was ipaddress.AddressValueError, itself a bare ValueError, now caught before it can be confused with the sibling ProtocolError/FieldValueError raises in this method bytearray is accepted alongside bytes for the six octet subtypes: it already round-trips correctly through struct.pack('Ns', ...) (measured), so rejecting it would tighten behaviour that was never broken. memoryview looks equally bytes-like but does not survive that same pack call (measured: same struct.error), so it is rejected with everything else. A str that is ASCII-only is not auto-encoded for the octet subtypes, and bytes is not auto-decoded for NAI: either would reintroduce the same "accepts a value that means the wrong thing" class of bug #467 removed for int, just relocated to bytes/str. test_mh_mn_id_option_rejects_wrong_type_identifier_per_subtype sweeps every subtype against every wrong type, including empty-value boundary cases, and is confirmed to fail against the pre-fix code (64 subtest failures) before passing against the fix. Build: tests/protocols/internet/test_mh_unit.py -> 38 passed, 408 subtests passed. Full suite (tests/) -> 1032 passed, 17 skipped, 1707 subtests passed. mypy --config-file mypy.ini pcapkit/protocols/internet/mh.py -> 97 errors (down from 98 baseline; the one resolved was this method's own len(identifier) Sized-type complaint), remaining two pre-existing and unrelated to this method.
|
Review at head Scope of the diff
CIWaited it out rather than judging a partial rollup. Final state, 21 SUCCESS + 2 SKIPPED-by-design + 1 StatusContext SUCCESS = fully green. Independent verification of the PR's own measurementsAll run myself in this worktree,
Attack 1 — the
|
…e overstating (#469) Both at the owner's request on the PR, on the two nits its review had raised as non-blocking. The docstring's Raises: bullet said "anything but bytes/bytearray for the other six", which read in isolation implies an int raises there too. It does not -- #468 accepts and converts an int for those six subtypes, and the Args: section above already says so. The bullet now names int alongside and points at #468, so the two halves of the docstring agree. bool is an int subclass, so True/False fell through to #468's numeric path and silently produced a plausible-looking wire form: IPv6Address(1), that is ::1, for IPv6_Address, and a one-octet identifier for the six BytesField subtypes. The review flagged this as out of scope and asked for no action; the owner asked for it fixed. An MN-ID of True is a caller mistake in every case rather than a value anyone means. The guard sits BEFORE the subtype dispatch, not inside the int branch -- my first attempt put it after, where it could not catch the IPv6_Address case at all, which is the same placement error #468's negative-int guard had to be corrected for. Measured all 8 subtypes x {True, False}: 16 combinations, every one now ProtocolError; the message names int(flag) as the escape hatch, and int(True) still converts to b'\x01'. Real ints are untouched: IPv6_Address 0x1234 -> IPv6Address('::1234'), IMSI 0x1234 -> b'\x124'. Verified: tests/protocols/internet/test_mh_unit.py 39 passed, 424 subtests, 0 failed. Revert-proof -- disabling only the bool guard fails 16 subtests of the new case and nothing else; restoring passes.
The bool guard shipped without the docstring that describes it, and the Args: sentence was left saying an int is accepted for every subtype but NAI -- which is now false, since bool is an int subclass and True is rejected. Reported on review of 35834b6. Args: carries the carve-out and points at Raises: for the reason. Raises: leads with it and says why rejecting is right rather than only that it happens: True would otherwise be converted by two different paths, to ::1 for IPv6_Address and to a one-octet identifier for the other six, and int(...) is the way to ask for the numeric value. This is the same class of defect the previous commit fixed two lines below -- a Raises: clause not matching behaviour, as #468 had to correct once already -- so it is worth fixing rather than filing. Docstring only; no behaviour change. Verified: 16 of 16 subtype/bool combinations still raise ProtocolError, IMSI 0x1234 still packs to b'\x124', tests/protocols/internet/test_mh_unit.py 39 passed / 424 subtests, and mypy --config-file mypy.ini reports the same two pre-existing errors on this file as before the change.
…tion (#500) Closes #491, the unfixed IPv4 sibling of #469/#481. - `_IPAddressField.pre_process`, `IPv4InterfaceField.pre_process`, and `IPv6InterfaceField.pre_process` (pcapkit/corekit/fields/ipaddress.py) now reject a `bool` value before dispatching into `ipaddress.ip_address`/ `ip_interface`, via a new shared `_reject_bool` helper. `bool` is an `int` subclass, and `ipaddress.ip_address()` treats any `int` below `2**32` as IPv4 -- so `True`/`False` used to be silently packed as `0.0.0.1`/ `0.0.0.0` on an IPv4-typed field, with no exception and no warning. On an IPv6-typed field the same conversion happened to raise instead (version mismatch), which is the asymmetry that let this slip past #481's guard for MH's `_make_opt_mn_id` -- that PR fixed the mechanism at one call site, not at its shared root. - `SecurityAssociation.__init__` (pcapkit/protocols/internet/esp.py) calls `ipaddress.ip_address()` directly on `destination`, bypassing the Field abstraction entirely; it gets the same guard, raising `ProtocolError`. - Both guards point the caller at `int(...)`, matching #481's message shape. tests/corekit/test_fields_ipaddress.py sweeps `{True, False}` across all four field classes (IPv4/IPv6 address and interface) and reproduces the original corruption through the public `IPv4.make(src=True, dst=False)` API. tests/protocols/internet/test_esp_unit.py adds the equivalent case for `SecurityAssociation`. Build: tests/corekit/test_fields_ipaddress.py -> 14 passed, 25 subtests. tests/protocols/internet/test_esp_unit.py -> 30 passed, 38 subtests. Full tests/corekit + tests/protocols/internet -> 254 passed, 662 subtests, 0 failed.
- `TSOption.post_process` converted `ts_data` entries to addresses with a bare
`ipaddress.ip_address`, which takes a `bool` as the `int` it subclasses, so
`ts_data=[True, 5]` packed and reported `IPv4Address('0.0.0.1')` with no
exception. It runs on the packing path too, and `IPv4.make` accepts a
caller-built option schema, so this was reachable from the public API. All
three conversions now go through `parse_ip_address`, the fifth site of the
defect #481, #500, #539 and #540 fixed before it.
- `quick_start_data_selector` sized the nested Quick-Start suboption with a
hardcoded `SchemaField(length=5)` -- the width of a Request's `ttl` and
`nonce` alone. A well-formed 8-octet option decoded its nonce as 55 rather
than 933982136 and left three octets to be read as a fabricated option, so
the datagram failed with `ProtocolError`. The length now comes from
`quick_start_option_length`, computed from the resolved suboption, and
`QuickStartReportOption` gains the RFC 4782 section 3.1 `Not Used` octet it
was missing, which had made it seven octets wide against the `length=8` both
`_make_opt_qs` and `_read_opt_qs` use.
- `_make_opt_ts` passed `data=` where the schema field is `ts_data`, so every
timestamp was dropped with an `UnknownFieldWarning` and the Timestamp option
was unbuildable through `make`. The `TYPE_CHECKING` `__init__` stub that
advertised `data` is corrected too.
Three new tests, each shown to fail without its fix; `ipv4-option/TS` deleted
from `EXPECTED_FAILURES` now that it round-trips. Full unit tier green, 1107
passed with 2666 subtests; both changed modules at 100% statement and branch
coverage.
- `TSOption.post_process` converted `ts_data` entries to addresses with a bare
`ipaddress.ip_address`, which takes a `bool` as the `int` it subclasses, so
`ts_data=[True, 5]` packed and reported `IPv4Address('0.0.0.1')` with no
exception. It runs on the packing path too, and `IPv4.make` accepts a
caller-built option schema, so this was reachable from the public API. All
three conversions now go through `parse_ip_address`, the fifth site of the
defect #481, #500, #539 and #540 fixed before it.
- `quick_start_data_selector` sized the nested Quick-Start suboption with a
hardcoded `SchemaField(length=5)` -- the width of a Request's `ttl` and
`nonce` alone. A well-formed 8-octet option decoded its nonce as 55 rather
than 933982136 and left three octets to be read as a fabricated option, so
the datagram failed with `ProtocolError`. The length now comes from
`quick_start_option_length`, computed from the resolved suboption, and
`QuickStartReportOption` gains the RFC 4782 section 3.1 `Not Used` octet it
was missing, which had made it seven octets wide against the `length=8` both
`_make_opt_qs` and `_read_opt_qs` use.
- `_make_opt_ts` passed `data=` where the schema field is `ts_data`, so every
timestamp was dropped with an `UnknownFieldWarning` and the Timestamp option
was unbuildable through `make`. The `TYPE_CHECKING` `__init__` stub that
advertised `data` is corrected too.
Three new tests, each shown to fail without its fix; `ipv4-option/TS` deleted
from `EXPECTED_FAILURES` now that it round-trips. Full unit tier green, 1107
passed with 2666 subtests; both changed modules at 100% statement and branch
coverage.
Closes #469
Summary
_make_opt_mn_iddocumentsidentifierasbytes | str | IPv6Address | int,but each MN-ID subtype's field (
mn_id_selector) accepts only one of the firsttwo: a
StringFieldforNAI, aBytesFieldfor the other six. Handing thewrong one -- or a value of neither type -- escaped as a bare stdlib exception
instead of a
ProtocolError. #468 already closed theinthalf of thisfamily (#467); this PR closes the rest, per #469's own scoping ("only its
remaining half" -- the
inthalf is untouched).What changed
pcapkit/protocols/internet/mh.py::MH._make_opt_mn_id:IPv6_Addressbranch now wraps theipaddress.IPv6Address(identifier)conversion in
try/except ValueError, re-raising asProtocolError. Thiscatches
ipaddress.AddressValueError(itself a bareValueError) for anyidentifier
ipaddress.IPv6Addresscannot parse, without swallowing thesibling
ProtocolErrorraised just above it for an out-of-rangeint.elif subtype_val == Enum_MNIDSubtype.NAIbranch rejects anynon-
stridentifier with aProtocolErrorbefore thelen()call thatused to leak
TypeError/AttributeError.elsebranch (the six octet subtypes) rejects any identifierthat isn't
bytes/bytearraywith aProtocolError, for the same reason.tests/protocols/internet/test_mh_unit.pygainstest_mh_mn_id_option_rejects_wrong_type_identifier_per_subtype, sweeping alleight subtypes against every wrong type (including empty-value boundaries),
and asserting the matching type still works.
Measurements
Re-measured myself on
e80c42217(currentmain) before changing anything,through the public maker, not a hand-built schema:
NAIb'\x01\x02',[1,2],{'a':1}AttributeErrorProtocolErrorNAI1.5,None,IPv6Address('::1')TypeErrorProtocolErrorNAIb''(empty)AttributeErrorProtocolError'user@realm',[1,2],{'a':1}struct.errorProtocolError1.5,None,IPv6Address('::1')TypeErrorProtocolError''(empty),memoryview(...)struct.errorProtocolErrorIPv6_Addressb'\x01\x02','user@realm',1.5,None,[1,2],{'a':1},b'',bytearray(16),memoryview(16)ipaddress.AddressValueError(aValueError)ProtocolErrorCorrect-type identifiers are unaffected:
strforNAI,bytes/bytearrayfor the six octet subtypes, an
IPv6Address/validint/validstrforIPv6_Address-- all still construct and pack, including the empty-valueboundary (
NAI+'', the six +b''). Theinthalf from #468 isunaffected: valid ints still convert per subtype, and the negative/
NAI/>=2**128rejections still raise exactly as before.Revert-proof: reverting only
mh.py(test file left in place) makes the newtest fail with 64 subtest failures across the wrong-type sweep; restoring the
fix makes it pass again.
Build:
tests/protocols/internet/test_mh_unit.py-> 38 passed, 408subtests passed. Full suite
tests/-> 1032 passed, 17 skipped, 1707 subtestspassed.
mypy --config-file mypy.ini pcapkit/protocols/internet/mh.py-> 97errors, down from a 98-error baseline on unmodified
main(the one resolvedis this method's own
len(identifier)"incompatible type... expected Sized"complaint, now that
isinstancenarrows the union before eachlen()call);the remaining 2 are pre-existing and unrelated to this method (lines outside
_make_opt_mn_id).Judgement call:
bytearray,memoryview, and ASCIIstrbytearrayis accepted alongsidebytesfor the six octet subtypes.Measured:
struct.pack('Ns', bytearray(...))already round-tripscorrectly today, so rejecting it would tighten behaviour that was never
broken, for no benefit.
memoryviewis rejected even though it looks equally bytes-like.Measured:
struct.pack('Ns', memoryview(...))raisesstruct.error: argument for 's' must be a bytes object-- it does notactually survive the pack call, so letting it through would just relocate
the leak rather than fix it.
strthat happens to be pure ASCII is not auto-encoded for theoctet subtypes, and a
bytesthat happens to decode cleanly is notauto-decoded for
NAI. Either direction reintroduces the same "accepts avalue that means the wrong thing" class of bug MH MN-ID: an int identifier cannot be packed for any subtype except IPv6_Address, and its declared length is wrong #467 removed for
int,just relocated to
bytes/str.Out of scope
Nothing further found in this half; no new issues filed.