Skip to content

Commit ee47dbe

Browse files
authored
fix(protocols): bound the IPv6 extension-header chain at 32 headers (#1604) (#1608)
1 parent d6f6274 commit ee47dbe

7 files changed

Lines changed: 779 additions & 30 deletions

File tree

‎docs/source/pcapkit/protocols/internet/internet.rst‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,25 @@ which is a base class for internet layer protocols, eg. :class:`~pcapkit.protoco
1818

1919
.. automethod:: _decode_next_layer
2020
.. automethod:: _import_next_layer
21+
.. automethod:: _next_layer_class
22+
.. automethod:: _next_exthdr_depth
2123

2224
.. autoattribute:: __layer__
2325
:no-value:
2426
.. autoattribute:: __proto__
2527
:no-value:
28+
.. autoattribute:: _exthdr_depth
29+
30+
Extension Header Chains
31+
-----------------------
32+
33+
A chain of IPv6 extension headers is dissected to at most
34+
:data:`~pcapkit.protocols.internet.internet.EXTENSION_HEADER_LIMIT` headers in
35+
a row, whether it follows IPv6 or another layer such as IPv4. What follows is
36+
kept as :class:`~pcapkit.protocols.misc.raw.Raw`, with one
37+
:exc:`~pcapkit.utilities.warnings.ProtocolWarning` (:issue:`1604`). The
38+
``pypcapfile`` toolkit stops at the same depth. The ``dpkt``, ``scapy`` and
39+
``pyshark`` engines leave the walk to their own parsers, which do not stop
40+
there, so they may still find the upper layer past it (:issue:`1609`).
41+
42+
.. autodata:: pcapkit.protocols.internet.internet.EXTENSION_HEADER_LIMIT

‎pcapkit/protocols/internet/internet.py‎

Lines changed: 115 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@
1515
import io
1616
from typing import TYPE_CHECKING, Generic, cast
1717

18+
from pcapkit.const.ipv6.extension_header import ExtensionHeader as Enum_ExtensionHeader
1819
from pcapkit.const.reg.transtype import TransType as Enum_TransType
1920
from pcapkit.corekit.module import ModuleDescriptor
2021
from pcapkit.corekit.protochain import ProtoChain
2122
from pcapkit.protocols.protocol import _PT, _ST, ProtocolBase
2223
from pcapkit.protocols.schema.schema import keep_short_read, replay_short_read
2324
from pcapkit.utilities.decorators import beholder
24-
from pcapkit.utilities.warnings import RegistryWarning, warn
25+
from pcapkit.utilities.exceptions import stacklevel
26+
from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn
2527

2628
if TYPE_CHECKING:
2729
from typing import IO, Any, Optional, Type
@@ -30,6 +32,24 @@
3032

3133
__all__ = ['Internet']
3234

35+
#: Most IPv6 extension headers dissected in one chain, i.e. one after another
36+
#: with no other layer between them (:issue:`1604`). The walk stops there, and
37+
#: what follows the last of them is kept as
38+
#: :class:`~pcapkit.protocols.misc.raw.Raw`, with one
39+
#: :exc:`~pcapkit.utilities.warnings.ProtocolWarning`. That holds whether the
40+
#: chain follows IPv6, which walks it in a loop
41+
#: (:meth:`IPv6._decode_next_layer
42+
#: <pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer>`), or another
43+
#: layer such as IPv4, where each header dissects the next one itself
44+
#: (:meth:`Internet._import_next_layer`). The toolkit adapters that walk a
45+
#: chain themselves stop at the same depth.
46+
#:
47+
#: :rfc:`8200#section-4.1` allows each extension header at most once, and the
48+
#: Destination Options header at most twice, so no compliant chain gets close.
49+
#: Without the bound, a chain dissected header by header stops wherever
50+
#: Python's recursion limit falls, which depends on the interpreter.
51+
EXTENSION_HEADER_LIMIT = 32
52+
3353

3454
class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=abstract-method
3555
"""Abstract base class for internet layer protocol family.
@@ -118,6 +138,12 @@ class Internet(ProtocolBase[_PT, _ST], Generic[_PT, _ST]): # pylint: disable=ab
118138
},
119139
)
120140

141+
#: int: How many IPv6 extension headers in a row the next layer follows:
142+
#: this layer's place in such a chain, or how many headers
143+
#: :class:`~pcapkit.protocols.internet.ipv6.IPv6` walked, else ``0``, c.f.
144+
#: :data:`EXTENSION_HEADER_LIMIT` and :meth:`_next_exthdr_depth`.
145+
_exthdr_depth = 0
146+
121147
##########################################################################
122148
# Properties.
123149
##########################################################################
@@ -197,6 +223,10 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None,
197223
file: Source packet stream.
198224
length: Length of packet data.
199225
extension: If the protocol is used as an IPv6 extension header.
226+
_exthdr_depth (int): Where this layer is in a chain of IPv6
227+
extension headers, counting from ``1``, or ``0`` if it is not
228+
in one. Handed over by :meth:`_import_next_layer` while
229+
parsing, c.f. :data:`EXTENSION_HEADER_LIMIT`.
200230
**kwargs: Arbitrary keyword arguments.
201231
202232
Notes:
@@ -211,6 +241,9 @@ def __post_init__(self, file: 'Optional[IO[bytes] | bytes]' = None,
211241
(:issue:`1458`).
212242
213243
"""
244+
# NOTE: Set before parsing, since parsing is what dissects the next layer.
245+
self._exthdr_depth = cast('int', kwargs.pop('_exthdr_depth', 0))
246+
214247
super().__post_init__(file, length, extension=extension, **kwargs) # type: ignore[arg-type]
215248
keep_short_read(self)
216249

@@ -237,6 +270,77 @@ def _read_protos(self, size: 'int') -> 'Enum_TransType':
237270
_prot = Enum_TransType.get(_byte)
238271
return _prot
239272

273+
def _next_exthdr_depth(self, proto: 'Optional[int]', extension: 'bool' = False) -> 'int':
274+
"""Place the next layer in its chain of IPv6 extension headers.
275+
276+
Arguments:
277+
proto: next layer protocol index
278+
extension: if the next layer is parsed as an extension header by
279+
:class:`~pcapkit.protocols.internet.ipv6.IPv6`'s walk, which
280+
counts the chain itself, c.f. :meth:`IPv6._decode_next_layer
281+
<pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer>`
282+
283+
Returns:
284+
One more than :attr:`_exthdr_depth` if ``proto`` names an IPv6
285+
extension header, or ``0`` if it names none, so that another layer
286+
between two headers starts a new chain. Past
287+
:data:`EXTENSION_HEADER_LIMIT`, the next layer is left raw.
288+
289+
"""
290+
if extension:
291+
return 0
292+
try:
293+
Enum_ExtensionHeader(proto)
294+
except ValueError:
295+
return 0
296+
return self._exthdr_depth + 1
297+
298+
def _next_layer_class(self, proto: 'Optional[int]', file_: 'bytes', length: 'int',
299+
extension: 'bool' = False) -> 'tuple[Type[ProtocolBase], dict[str, Any]]':
300+
r"""Pick the class the next layer is parsed with.
301+
302+
Arguments:
303+
proto: next layer protocol index
304+
file\_: payload of the current layer
305+
length: valid (*non-padding*) length
306+
extension: if is extension header, c.f. :meth:`_next_exthdr_depth`
307+
308+
Returns:
309+
:class:`~pcapkit.protocols.misc.null.NoPayload` if there is
310+
nothing to parse; :class:`~pcapkit.protocols.misc.raw.Raw` if the
311+
parse stops at this layer, or the next layer is an IPv6 extension
312+
header past :data:`EXTENSION_HEADER_LIMIT` in a row; otherwise the
313+
class registered for ``proto``. With it, the keywords only that next
314+
layer is constructed with: the warning given, as ``error``, or the
315+
next layer's place in its chain, as ``_exthdr_depth``.
316+
317+
Warns:
318+
ProtocolWarning: If the next layer is past
319+
:data:`EXTENSION_HEADER_LIMIT` (:issue:`1604`).
320+
321+
"""
322+
if min(length, len(file_)) == 0:
323+
from pcapkit.protocols.misc.null import NoPayload # isort: skip # pylint: disable=import-outside-toplevel
324+
return NoPayload, {}
325+
if self._sigterm:
326+
from pcapkit.protocols.misc.raw import Raw # isort: skip # pylint: disable=import-outside-toplevel
327+
return Raw, {}
328+
329+
depth = self._next_exthdr_depth(proto, extension)
330+
if depth > EXTENSION_HEADER_LIMIT:
331+
from pcapkit.protocols.misc.raw import Raw # isort: skip # pylint: disable=import-outside-toplevel
332+
333+
message = (f'{self.alias}: a chain of IPv6 extension headers reached the limit of '
334+
f'{EXTENSION_HEADER_LIMIT} headers; the rest of the packet, from Next '
335+
f'Header {int(cast("int", proto))}, is left undissected')
336+
warn(message, ProtocolWarning, stacklevel=stacklevel())
337+
return Raw, {'error': message}
338+
339+
protocol = self._lookup_next_layer(self.__proto__, cast('int', proto))
340+
if depth and issubclass(protocol, Internet):
341+
return protocol, {'_exthdr_depth': depth}
342+
return protocol, {}
343+
240344
def _decode_next_layer(self, dict_: '_PT', proto: 'Optional[int]' = None, # pylint: disable=arguments-differ
241345
length: 'Optional[int]' = None, *, packet: 'Optional[dict[str, Any]]' = None,
242346
version: 'Literal[4, 6]' = 4, ipv6_exthdr: 'Optional[ProtoChain]' = None,
@@ -304,24 +408,23 @@ def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, #
304408
Returns:
305409
Instance of next layer.
306410
307-
"""
308-
if TYPE_CHECKING:
309-
protocol: 'Type[ProtocolBase]'
411+
Notes:
412+
An IPv6 extension header past :data:`EXTENSION_HEADER_LIMIT` in a
413+
row is not dissected: it and the rest of the packet are kept as
414+
:class:`~pcapkit.protocols.misc.raw.Raw`, with one
415+
:exc:`~pcapkit.utilities.warnings.ProtocolWarning` (:issue:`1604`).
416+
Each header dissects the next one from here, so the chain's depth
417+
is handed to the next layer, c.f. :meth:`_next_layer_class`.
310418
419+
"""
311420
if payload is None:
312421
file_ = self.__header__.get_payload()
313422
else:
314423
file_ = payload
315424
if length is None:
316425
length = len(file_)
317426

318-
if min(length, len(file_)) == 0:
319-
from pcapkit.protocols.misc.null import NoPayload as protocol # isort: skip # pylint: disable=import-outside-toplevel
320-
elif self._sigterm:
321-
from pcapkit.protocols.misc.raw import Raw as protocol # isort: skip # pylint: disable=import-outside-toplevel
322-
else:
323-
protocol = self._lookup_next_layer(self.__proto__, proto)
324-
427+
protocol, extra = self._next_layer_class(proto, file_, length, extension)
325428
return self._parse_next_layer(protocol, file_, length, version=version, extension=extension,
326429
alias=proto, packet=packet, layer=self._exlayer,
327-
protocol=self._exproto, __context__=self._exctx)
430+
protocol=self._exproto, __context__=self._exctx, **extra)

‎pcapkit/protocols/internet/ipv6.py‎

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from pcapkit.corekit.multidict import MultiDict, OrderedMultiDict
3636
from pcapkit.corekit.protochain import ProtoChain
3737
from pcapkit.protocols.data.internet.ipv6 import IPv6 as Data_IPv6
38+
from pcapkit.protocols.internet.internet import EXTENSION_HEADER_LIMIT
3839
from pcapkit.protocols.internet.ip import IP
3940
from pcapkit.protocols.schema.internet.ipv6 import IPv6 as Schema_IPv6
4041
from pcapkit.protocols.schema.internet.ipv6 import jumbo_payload_length
@@ -530,7 +531,7 @@ def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None,
530531

531532
hdr_len = self.length # header length
532533
raw_len = ipv6.payload if length is None else length # payload length
533-
_protos = [] # ProtoChain buffer
534+
_protos = [] # type: list[ProtocolBase] # ProtoChain buffer
534535
_exthdr = [] # (parser class, info) per extension header
535536
split = None # type: Optional[tuple[int, int]] # at first Fragment header
536537
opaque = False # walk ended at a non-zero Fragment Offset
@@ -543,6 +544,15 @@ def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None,
543544
except ValueError:
544545
break
545546

547+
# NOTE: At most :data:`EXTENSION_HEADER_LIMIT
548+
# <pcapkit.protocols.internet.internet.EXTENSION_HEADER_LIMIT>`
549+
# headers are walked, as at most that many are dissected after any
550+
# other layer. This header and the rest are then the upper layer,
551+
# which :meth:`_import_next_layer` keeps raw, with one warning
552+
# (:issue:`1604`).
553+
if len(_protos) >= EXTENSION_HEADER_LIMIT:
554+
break
555+
546556
# # directly break when No Next Header occurs
547557
# if proto.name == 'IPv6-NoNxt':
548558
# proto = None
@@ -646,6 +656,7 @@ def _decode_next_layer(self, ipv6: 'Data_IPv6', proto: 'Optional[int]' = None,
646656
# of a slice of UDP payload (:issue:`1545`). Reassembly takes
647657
# ``fragment`` above, which this leaves as it was.
648658
ipv6_exthdr = ProtoChain.from_list(_protos) # type: ignore[arg-type]
659+
self._exthdr_depth = len(_protos) # the headers the upper layer follows
649660
return super()._decode_next_layer(ipv6, None if opaque else proto, raw_len, packet=packet,
650661
ipv6_exthdr=ipv6_exthdr, payload=payload)
651662

@@ -689,30 +700,27 @@ def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, #
689700
ends after it -- see :attr:`__generic_ext_codes__`'s docstring for
690701
the full distinction.
691702
692-
"""
693-
if TYPE_CHECKING:
694-
protocol: 'Type[ProtocolBase]'
703+
The upper layer past a walk cut at :data:`EXTENSION_HEADER_LIMIT
704+
<pcapkit.protocols.internet.internet.EXTENSION_HEADER_LIMIT>` is the
705+
next extension header, and is kept raw with one
706+
:exc:`~pcapkit.utilities.warnings.ProtocolWarning`, as in
707+
:meth:`Internet._import_next_layer
708+
<pcapkit.protocols.internet.internet.Internet._import_next_layer>`
709+
(:issue:`1604`).
695710
711+
"""
696712
if payload is None:
697713
file_ = self.__header__.get_payload()
698714
else:
699715
file_ = payload
700716
if length is None:
701717
length = len(file_)
702718

703-
if min(length, len(file_)) == 0:
704-
from pcapkit.protocols.misc.null import \
705-
NoPayload as protocol # isort: skip # pylint: disable=import-outside-toplevel
706-
elif self._sigterm:
707-
from pcapkit.protocols.misc.raw import \
708-
Raw as protocol # isort: skip # pylint: disable=import-outside-toplevel
709-
else:
710-
protocol = self._lookup_next_layer(self.__proto__, proto)
711-
719+
protocol, extra = self._next_layer_class(proto, file_, length, extension)
712720
try:
713721
next_ = self._parse_next_layer(protocol, file_, length, version=version, extension=extension,
714722
alias=proto, packet=packet, layer=self._exlayer,
715-
protocol=self._exproto, __context__=self._exctx)
723+
protocol=self._exproto, __context__=self._exctx, **extra)
716724
except Exception as exc:
717725
from pcapkit.protocols.internet.ipv6_ext import \
718726
IPv6_Ext # isort: skip # pylint: disable=import-outside-toplevel

‎pcapkit/toolkit/pypcapfile.py‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
from pcapkit.foundation.reassembly.data.ip import Packet as IP_Packet
7474
from pcapkit.foundation.reassembly.data.tcp import Packet as TCP_Packet
7575
from pcapkit.foundation.traceflow.data.tcp import Packet as TF_TCP_Packet
76+
from pcapkit.protocols.internet.internet import EXTENSION_HEADER_LIMIT
7677
from pcapkit.protocols.internet.ipv4 import IPv4 as Protocol_IPv4
7778
from pcapkit.protocols.schema.internet.ipv6 import jumbo_payload_length
7879
from pcapkit.protocols.transport.tcp import TCP as Protocol_TCP
@@ -659,7 +660,10 @@ def _past_extension_headers(protocol: 'int', data: 'bytes') -> 'Optional[tuple[i
659660
default engine leaves one of them undissected. An AH is
660661
``(Payload Length + 2) * 4`` octets long (:rfc:`4302#section-2.2`), and
661662
no shorter than :data:`AH_HEADER_LEN`; any other is read by
662-
:func:`_default_extension_header` (:issue:`1597`).
663+
:func:`_default_extension_header` (:issue:`1597`). Past
664+
:data:`~pcapkit.protocols.internet.internet.EXTENSION_HEADER_LIMIT`
665+
of them in a row, the default engine leaves the rest undissected too
666+
(:issue:`1604`).
663667
664668
Note:
665669
Inside IPv4, the default engine decodes what an IPv6 Fragment header
@@ -668,7 +672,11 @@ def _past_extension_headers(protocol: 'int', data: 'bytes') -> 'Optional[tuple[i
668672
they are there.
669673
670674
"""
675+
depth = 0
671676
while protocol in IPV4_EXTENSION_HEADERS:
677+
if depth == EXTENSION_HEADER_LIMIT:
678+
return None
679+
depth += 1
672680
if protocol == Enum_TransType.AH:
673681
length = (data[1] + 2) * 4 if len(data) >= AH_HEADER_LEN else 0
674682
if length < AH_HEADER_LEN or len(data) < length:
@@ -704,7 +712,9 @@ def _upper_layer(version: 'int', data: 'bytes') -> 'Optional[tuple[int, bytes]]'
704712
Fragment header as if that header were not there. A later fragment of
705713
either has no upper-layer header, since from its offset on it carries a
706714
slice of the payload (:rfc:`791#section-3.2`, :rfc:`8200#section-4.5`).
707-
Nor does a truncated header or header chain.
715+
Nor does a truncated header or header chain, or a chain of more than
716+
:data:`~pcapkit.protocols.internet.internet.EXTENSION_HEADER_LIMIT`
717+
extension headers (:issue:`1604`).
708718
709719
Note:
710720
Each of these rules is the default engine's own, as are the ones
@@ -737,10 +747,11 @@ def _upper_layer(version: 'int', data: 'bytes') -> 'Optional[tuple[int, bytes]]'
737747
if not extent:
738748
extent = jumbo_payload_length(data[6], data[IPV6_HEADER_LEN:]) or 0
739749
data = data[:IPV6_HEADER_LEN + extent]
740-
nxt, offset = data[6], IPV6_HEADER_LEN
750+
nxt, offset, depth = data[6], IPV6_HEADER_LEN, 0
741751
while nxt in IPV6_EXTENSION_HEADERS:
742-
if len(data) < offset + 8:
752+
if depth == EXTENSION_HEADER_LIMIT or len(data) < offset + 8:
743753
return None
754+
depth += 1
744755
if nxt == Enum_ExtensionHeader.IPv6_Frag: # fixed length, second octet reserved
745756
if struct.unpack_from('!H', data, offset + 2)[0] >> 3: # a later fragment
746757
return None

0 commit comments

Comments
 (0)