Bug #9186
openhttp: gap before protocol detection permanently skips the HTTP request behind it
Description
Summary¶
If a TCP direction is still ALPROTO_UNKNOWN, one ACK'd sequence hole marks app-layer protocol detection complete and returns failure before any protocol bytes are parsed. STREAM_START is never attached again, so TCPProtoDetect does not run, and a negative gap result also skips the bytes that sit behind the hole in that same pass. A full HTTP request placed immediately after the hole produces no alert and no HTTP transaction, while the same request with no hole is alerted and logged. The engine keeps running. Unpack the attachment and run ./verify.sh.
CWE¶
- CWE-693 Protection Mechanism Failure
CVSS 3.1: 7.5 (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N)
Affected Versions¶
Confirmed on jasonish/suricata:latest, Suricata 8.0.7 RELEASE, in default IDS mode. Stream reassembly and HTTP are left at their defaults. No extra app-layer protocol was enabled. -k none is only there because veth checksum offload marks ordinary frames invalid on the bridge. flow-timeouts.tcp.closed=1 and stats.interval=1 only make the flow record and the tcp.reassembly_gap counter show up before the test ends.
Preconditions¶
The sensor must see the handshake, then a toserver hole before any application payload, then a complete HTTP request, then an ACK that covers both the hole and that request. IDS mode only feeds ACK'd data to the app layer, so a real server stack that never received the missing bytes will not ACK over the hole. The lab injects that ACK on the monitored bridge. The same pattern is what the sensor sees when it misses the leading bytes and the peer still ACKs them. No account is required. A toclient HTTP response can still let the opposite direction identify the protocol, so the attack flow does not elicit one.
Security Impact¶
The toserver direction stops protocol detection for the rest of the flow. The HTTP rule does not fire and no http event is written. The flow record shows ts_gap and alerted: false, and app_proto stays unset. tcp.reassembly_gap increments, which is the counter next to the gap handoff. The process does not crash. This run does not show memory corruption, a second-direction bypass after the peer has already identified the protocol, or code execution.
Vulnerable Code¶
src/app-layer.c, gap branch in AppLayerHandleTCPData. A hole on an unknown direction sets the completed flag and fails out while f->alproto is still unknown, so progress is not advanced and the parser is not called.
if (flags & STREAM_GAP) {
if (alproto == ALPROTO_UNKNOWN) {
StreamTcpSetStreamFlagAppProtoDetectionCompleted(*stream);
if (f->alproto == ALPROTO_UNKNOWN) {
goto failure;
}
}
}
src/stream-tcp-reassemble.c then drops the rest of the pass. StreamGetAppLayerFlags adds STREAM_START only while the completed flag is clear, and TCPProtoDetect requires both ALPROTO_UNKNOWN and STREAM_START. Later bytes hit the generic branch and are parsed only when f->alproto is already known.
int r = AppLayerHandleTCPData(..., StreamGetAppLayerFlags(...) | STREAM_GAP, ...);
if (r < 0)
return 0;
Call Chain¶
HandleEstablishedPacketToClient (src/stream-tcp.c:3109) -> StreamTcpReassembleHandleSegment (src/stream-tcp-reassemble.c:2008) -> ReassembleUpdateAppLayer (src/stream-tcp-reassemble.c:1263) -> gap handoff (src/stream-tcp-reassemble.c:1292) -> AppLayerHandleTCPData (src/app-layer.c:742).
Payload¶
After the three-way handshake, the first toserver payload starts 100 bytes past the initial sequence, and the payload is the whole request:
GET /pregap-marker HTTP/1.1
Host: app
Connection: close
An ACK from the server side then covers ISN+1+100+len(request). The control flow sends that same request at ISN+1 with no hole.
Reproduction¶
jasonish/suricata:latest sniffs bridge br-pregap on 172.31.4.0/24. The HTTP sink is 172.31.4.20:18080, also published on the host as 127.0.0.1:18080. Host-network capture is required because the sensor has to see both directions on the bridge.
./verify.sh
control_alerts=1
control_http=1
attack_alerts=0
attack_http=0
nic_post_gap=True
reassembly_gap=1
suricata_running=True
VERDICT: PASS
The attack flow record has flow_ts_gap=true, flow_alerted=false, and no app_proto. The bridge capture shows the request in one segment at ISN+1+100.
Fix Recommendation¶
Do not set APPPROTO_DETECTION_COMPLETED for a gap that arrives while that direction's alproto is still ALPROTO_UNKNOWN. Advance app_progress over the hole without closing detection, and do not return 0 from ReassembleUpdateAppLayer before the bytes behind the hole are delivered. Those bytes still need STREAM_START so TCPProtoDetect runs. If a leading hole is treated as "detection impossible", emit the same explicit skipped-detection event the bail path already emits, and do not fail silently.
Files
PA Updated by Philippe Antoine about 4 hours ago
- Tracker changed from Security to Bug
- Status changed from New to Assigned
- Assignee changed from OISF Dev to Philippe Antoine
- Private changed from Yes to No
- Disclosure Date deleted (
01/07/2027)
TCP gaps are not attacked controlled, so this is a bug