Skip to content

fix: reject a bool IP address in-library instead of silently corrupting the packet - #500

Merged
JarryShaw merged 2 commits into
mainfrom
fix/491-reject-bool-ip-address
Sep 19, 2026
Merged

JarryShaw merged 2 commits into
mainfrom
fix/491-reject-bool-ip-address

Conversation

@JarryShaw

Copy link
Copy Markdown
Owner

Fixes #491 — the unfixed shared root of #469/#481, which fixed the same mechanism at exactly one call site.

The defect

bool is an int subclass, and ipaddress.ip_address() treats any int below 2**32 as IPv4. So on an IPv4-typed field a bool was silently accepted and corrupted the packet — no exception, no warning:

IPv4.make(src=True, dst=False).pack()  ->  4500001400000000001100000000000100000000
decoded back                           ->  src=0.0.0.1  dst=0.0.0.0

The IPv4/IPv6 asymmetry is why #481 looked sufficient. On an IPv6-typed field the same conversion happens to raise, because the resulting IPv4Address's version mismatches — so the IPv6 half masked the IPv4 half, and fixing MH._make_opt_mn_id alone left the root untouched.

The fix

A shared _reject_bool(value, description) helper in pcapkit/corekit/fields/ipaddress.py, following the module's existing _reraise_as_field_value_error pattern, called as the first statement of _IPAddressField.pre_process, IPv4InterfaceField.pre_process and IPv6InterfaceField.pre_process — before any ipaddress.* dispatch.

That placement is deliberate and matches #481's: a correct check in the wrong position does not fire, and that exact mistake has been made twice in this repository's history with a passing test each time.

pcapkit/protocols/internet/esp.py gets the equivalent guard in SecurityAssociation.__init__, ahead of its direct ipaddress.ip_address() call, which sits outside the Field abstraction entirely and had no version check at all.

Exception choice

FieldValueError in the fields module, ProtocolError in esp.py — each module's existing convention, and ProtocolError is what #481 used for the same mechanism in a protocol module.

Deliberately not pcapkit.utilities.exceptions.BoolError: its docstring is "The argument(s) must be bool type" — it is for the inverse case, where a bool was expected and something else arrived. Using it here would invert its meaning.

Verification

Every public field class in the module, enumerated rather than spot-checked, with pcapkit.__file__ asserted against this branch before import:

IPv4AddressField.pre_process(True/False)   -> FieldValueError
IPv6AddressField.pre_process(True/False)   -> FieldValueError
IPv4InterfaceField.pre_process(True/False) -> FieldValueError
IPv6InterfaceField.pre_process(True/False) -> FieldValueError

Both original reproductions:

IPv4.make(src=True, dst=False)          -> FieldValueError: invalid IP address: must not be a bool,
                                           not True -- pass int(True) if the numeric value is what is wanted
SecurityAssociation(spi=1, destination=True) -> ProtocolError: invalid destination: must not be a bool, ...

The escape hatch the message promises actually works, and ordinary input is unaffected:

pre_process(int(True))    -> b'\x00\x00\x00\x01'
pre_process(int(False))   -> b'\x00\x00\x00\x00'
pre_process(1)            -> b'\x00\x00\x00\x01'
pre_process('10.0.0.1')   -> b'\n\x00\x00\x01'
pre_process(IPv4Address)  -> b'\n\x00\x00\x01'
interface('10.0.0.1/24')  -> b'\n\x00\x00\x01\xff\xff\xff\x00'

Revert-proof. Neutering the three field guards and the esp guard in place:

11 failed, 43 passed, 53 subtests passed
  SUBFAILED(field='IPv6InterfaceField', value=True) ...reject_a_bool_value_for_every_version
  FAILED ...test_ipv4_make_rejects_a_bool_address_through_the_public_api
  SUBFAILED(destination=True) ...test_security_association_rejects_a_bool_destination

Restored: 44 passed, 63 subtests passed.

The new tests sweep {True, False} across the address and interface types for both IP versions, because the IPv6-raises/IPv4-corrupts asymmetry is precisely what let this survive #481.

Scope deliberately left out

The roughly ten further IPv4-typed maker sites listed in #491 as "confirmed by inspection but not executed" are untouched here: RROption/LSROption/SSROption in ipv4.py, the dual-stack option makers in mh.py, and SCTP._make_param_ipv4. They share this mechanism and very likely still corrupt, but those files are being edited concurrently for #490/#493/#494, and splitting the change across them would have produced conflicting edits to the same lines.

With the root guarded, every one of those sites that routes through an IPv4AddressField is now covered automatically. SCTP._make_param_ipv4 and any site calling ipaddress.* directly are not, and want a follow-up pass once the other streams land.

…tion

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.
@JarryShaw
JarryShaw merged commit f09afba into main Sep 19, 2026
21 of 22 checks passed
@JarryShaw
JarryShaw deleted the fix/491-reject-bool-ip-address branch September 19, 2026 03:20
JarryShaw added a commit that referenced this pull request Sep 20, 2026
…508)

- add `parse_ip_address()` to `pcapkit.corekit.fields.ipaddress`, which calls
  the existing `_reject_bool` before converting, and takes an optional
  `version` so a caller that pins the address family widens an `int` to the
  right one
- route the seven `_make_*` sites that convert a caller-supplied address
  *before* the schema is built through it: `MH._make_opt_bid`,
  `MH._make_opt_lmaa`, `MH._make_fid_suboption`, `MH._make_opt_dmnp`,
  `MH._make_opt_lma_up`, `HIP._make_param_locator_set` and
  `TCP._make_mptcp_addaddr`. Each derives its option length or family flag
  from the converted address, so a bare `ipaddress.ip_address(True)` became
  `0.0.0.1` and #500's field-level guard could no longer tell it from a real
  address
- leave `SwitchField` untouched: its `pre_process` delegates to the resolved
  field and is not reached on the pack path at all, so the guard #508
  proposes putting there would be dead code
- drop the now-unused `import ipaddress` from `tcp.py`, and document
  `parse_ip_address` on the ipaddress fields page

Adds tests over three files, including one that re-derives the ten
address-typed `SwitchField` declarations from `Schema.__fields__` so a new
one cannot be added unnoticed. CI-equivalent unit suite: 1056 passed, with
only the pre-existing `test_docstring_contract` failure that main already has.
JarryShaw added a commit that referenced this pull request Sep 20, 2026
- `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.
JarryShaw added a commit that referenced this pull request Sep 20, 2026
- `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.
@JarryShaw JarryShaw added fix Pull requests that fix a defect (fix: subject prefix) breaking Breaks public-facing behaviour or API (apply alongside the type label) labels Sep 22, 2026
@JarryShaw JarryShaw added this to the 1.5 milestone Oct 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking Breaks public-facing behaviour or API (apply alongside the type label) fix Pull requests that fix a defect (fix: subject prefix)

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

IP address fields silently accept bool and corrupt the packet — unfixed sibling of #469/#481 at the shared root

1 participant