From 16c6e364edf9dcff0d15e2e2eaaec8ba326eedc8 Mon Sep 17 00:00:00 2001 From: Afloat <132809511+Afloat16@users.noreply.github.com> Date: Wed, 4 Nov 2026 01:18:22 +0700 Subject: [PATCH] Reject invalid TCP header lengths (from #712) --- crates/formats/src/pcap.rs | 15 ++++-- .../tests/regression_tcp_header_length.rs | 52 +++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 crates/formats/tests/regression_tcp_header_length.rs diff --git a/crates/formats/src/pcap.rs b/crates/formats/src/pcap.rs index 09c18126..cc981f02 100644 --- a/crates/formats/src/pcap.rs +++ b/crates/formats/src/pcap.rs @@ -652,10 +652,17 @@ fn transport(proto: u8, p: &[u8], d: &mut Decoded) { }; d.protocols.push("tcp"); let (seq, ack, off, flags, win) = (r.u32_be(4), r.u32_be(8), r.u8(12), r.u8(13), r.u16_be(14)); - if win.is_none() { - d.note = Some("TCP header cut short".into()); - } - let payload = off.map_or(&[][..], |off| p.get(usize::from(off >> 4) * 4..).unwrap_or(&[])); + let payload = match off.map(|off| usize::from(off >> 4) * 4) { + Some(length) if length < 20 => { + d.note = Some("TCP data offset is smaller than its header".into()); + &[][..] + } + Some(length) if length <= p.len() => p.get(length..).unwrap_or(&[]), + _ => { + d.note = Some("TCP header cut short".into()); + &[][..] + } + }; d.transport = Some(Transport { protocol: "tcp", src_port: Some(sp), diff --git a/crates/formats/tests/regression_tcp_header_length.rs b/crates/formats/tests/regression_tcp_header_length.rs new file mode 100644 index 00000000..18931811 --- /dev/null +++ b/crates/formats/tests/regression_tcp_header_length.rs @@ -0,0 +1,52 @@ +use craft_anything_formats::pcap::{Decoded, decode}; +use craft_anything_testkit::pcap::{ipv4_pkt, tcp}; + +fn decoded(segment: &[u8]) -> Decoded { + decode(101, &ipv4_pkt(6, [192, 0, 2, 1], [192, 0, 2, 2], segment)) +} + +#[test] +fn a_tcp_header_shorter_than_twenty_bytes_is_not_payload() { + for words in 0..5u8 { + let mut segment = tcp(51000, 80, 0x18, b"GET / HTTP/1.1\r\n\r\n"); + segment[12] = words << 4; + let result = decoded(&segment); + let transport = result.transport.unwrap(); + assert_eq!(transport.payload_len, 0, "offset {words}"); + assert_eq!((transport.src_port, transport.dst_port), (Some(51000), Some(80))); + assert!(result.http.is_none() && result.tls.is_none()); + assert!(result.note.is_some()); + } +} + +#[test] +fn a_cut_tcp_header_keeps_ports_but_reports_the_cut() { + let whole = tcp(51000, 80, 0x18, b"request"); + for end in 4..20 { + let result = decoded(&whole[..end]); + let transport = result.transport.unwrap(); + assert_eq!((transport.src_port, transport.dst_port), (Some(51000), Some(80))); + assert_eq!(transport.payload_len, 0); + assert!(result.note.is_some(), "cut at {end}"); + } + for words in 6..=15u8 { + let mut segment = tcp(51000, 80, 0x18, &[]); + segment[12] = words << 4; + let result = decoded(&segment); + assert_eq!(result.transport.unwrap().payload_len, 0); + assert!(result.note.is_some(), "missing options for offset {words}"); + } +} + +#[test] +fn a_valid_tcp_header_with_options_finds_its_payload() { + let mut segment = tcp(51000, 80, 0x18, &[]); + segment[12] = 7 << 4; + segment.extend([1; 8]); + let payload = b"GET /resource HTTP/1.1\r\nHost: example.test\r\n\r\n"; + segment.extend(payload); + let result = decoded(&segment); + assert_eq!(result.transport.unwrap().payload_len, payload.len()); + assert_eq!(result.http.unwrap().target.as_deref(), Some("/resource")); + assert!(result.note.is_none()); +}