Skip to content

Commit

Permalink
http2: handle reassembly for continuation frames
Browse files Browse the repository at this point in the history
Ticket: 5926

HTTP2 continuation frames are defined in RFC 9113.
They allow header blocks to be split over multiple HTTP2 frames.
For Suricata to process correctly these header blocks, it
must do the reassembly of the payload of these HTTP2 frames.
Otherwise, we get incomplete decoding for headers names and/or
values while decoding a single frame.

Design is to add a field to the HTTP2 state, as the RFC states that
these continuation frames form a discrete unit :
> Field blocks MUST be transmitted as a contiguous sequence of frames,
> with no interleaved frames of any other type or from any other stream.
So, we do not have to duplicate this reassembly field per stream id.

Another design choice is to wait for the reassembly to be complete
before doing any decoding, to avoid quadratic complexity on partially
decoding of the data.

(cherry picked from commit aff54f2)
  • Loading branch information
catenacyber authored and victorjulien committed Feb 6, 2024
1 parent 3cdd500 commit 478a2a3
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 4 deletions.
1 change: 1 addition & 0 deletions rules/http2-events.rules
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ alert http2 any any -> any any (msg:"SURICATA HTTP2 variable-length integer over
alert http2 any any -> any any (msg:"SURICATA HTTP2 too many streams"; flow:established; app-layer-event:http2.too_many_streams; classtype:protocol-command-decode; sid:2290012; rev:1;)
alert http2 any any -> any any (msg:"SURICATA HTTP2 authority host mismatch"; flow:established,to_server; app-layer-event:http2.authority_host_mismatch; classtype:protocol-command-decode; sid:2290013; rev:1;)
alert http2 any any -> any any (msg:"SURICATA HTTP2 user info in uri"; flow:established,to_server; app-layer-event:http2.userinfo_in_uri; classtype:protocol-command-decode; sid:2290014; rev:1;)
alert http2 any any -> any any (msg:"SURICATA HTTP2 reassembly limit reached"; flow:established; app-layer-event:http2.reassembly_limit_reached; classtype:protocol-command-decode; sid:2290015; rev:1;)
86 changes: 82 additions & 4 deletions rust/src/http2/http2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ const HTTP2_FRAME_RSTSTREAM_LEN: usize = 4;
const HTTP2_FRAME_PRIORITY_LEN: usize = 5;
const HTTP2_FRAME_WINDOWUPDATE_LEN: usize = 4;
pub static mut HTTP2_MAX_TABLESIZE: u32 = 65536; // 0x10000
// maximum size of reassembly for header + continuation
static mut HTTP2_MAX_REASS: usize = 102400;
static mut HTTP2_MAX_STREAMS: usize = 4096; // 0x1000

#[repr(u8)]
Expand Down Expand Up @@ -406,6 +408,7 @@ pub enum HTTP2Event {
TooManyStreams,
AuthorityHostMismatch,
UserinfoInUri,
ReassemblyLimitReached,
}

pub struct HTTP2DynTable {
Expand All @@ -432,6 +435,12 @@ impl HTTP2DynTable {
}
}

#[derive(Default)]
struct HTTP2HeaderReassemblyBuffer {
data: Vec<u8>,
stream_id: u32,
}

pub struct HTTP2State {
state_data: AppLayerStateData,
tx_id: u64,
Expand All @@ -441,6 +450,9 @@ pub struct HTTP2State {
dynamic_headers_tc: HTTP2DynTable,
transactions: VecDeque<HTTP2Transaction>,
progress: HTTP2ConnectionState,

c2s_buf: HTTP2HeaderReassemblyBuffer,
s2c_buf: HTTP2HeaderReassemblyBuffer,
}

impl State<HTTP2Transaction> for HTTP2State {
Expand Down Expand Up @@ -473,6 +485,8 @@ impl HTTP2State {
dynamic_headers_tc: HTTP2DynTable::new(),
transactions: VecDeque::new(),
progress: HTTP2ConnectionState::Http2StateInit,
c2s_buf: HTTP2HeaderReassemblyBuffer::default(),
s2c_buf: HTTP2HeaderReassemblyBuffer::default(),
}
}

Expand Down Expand Up @@ -686,8 +700,11 @@ impl HTTP2State {
}

fn parse_frame_data(
&mut self, ftype: u8, input: &[u8], complete: bool, hflags: u8, dir: Direction,
&mut self, head: &parser::HTTP2FrameHeader, input: &[u8], complete: bool, dir: Direction,
reass_limit_reached: &mut bool,
) -> HTTP2FrameTypeData {
let ftype = head.ftype;
let hflags = head.flags;
match num::FromPrimitive::from_u8(ftype) {
Some(parser::HTTP2FrameType::GoAway) => {
if input.len() < HTTP2_FRAME_GOAWAY_LEN {
Expand Down Expand Up @@ -847,17 +864,47 @@ impl HTTP2State {
return HTTP2FrameTypeData::DATA;
}
Some(parser::HTTP2FrameType::Continuation) => {
let buf = if dir == Direction::ToClient {
&mut self.s2c_buf
} else {
&mut self.c2s_buf
};
if head.stream_id == buf.stream_id {
let max_reass = unsafe { HTTP2_MAX_REASS };
if buf.data.len() + input.len() < max_reass {
buf.data.extend(input);
} else if buf.data.len() < max_reass {
buf.data.extend(&input[..max_reass - buf.data.len()]);
*reass_limit_reached = true;
}
if head.flags & parser::HTTP2_FLAG_HEADER_END_HEADERS == 0 {
let hs = parser::HTTP2FrameContinuation {
blocks: Vec::new(),
};
return HTTP2FrameTypeData::CONTINUATION(hs);
}
} // else try to parse anyways
let input_reass = if head.stream_id == buf.stream_id { &buf.data } else { input };

let dyn_headers = if dir == Direction::ToClient {
&mut self.dynamic_headers_tc
} else {
&mut self.dynamic_headers_ts
};
match parser::http2_parse_frame_continuation(input, dyn_headers) {
match parser::http2_parse_frame_continuation(input_reass, dyn_headers) {
Ok((_, hs)) => {
if head.stream_id == buf.stream_id {
buf.stream_id = 0;
buf.data.clear();
}
self.process_headers(&hs.blocks, dir);
return HTTP2FrameTypeData::CONTINUATION(hs);
}
Err(Err::Incomplete(_)) => {
if head.stream_id == buf.stream_id {
buf.stream_id = 0;
buf.data.clear();
}
if complete {
self.set_event(HTTP2Event::InvalidFrameData);
return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled {
Expand All @@ -870,6 +917,10 @@ impl HTTP2State {
}
}
Err(_) => {
if head.stream_id == buf.stream_id {
buf.stream_id = 0;
buf.data.clear();
}
self.set_event(HTTP2Event::InvalidFrameData);
return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled {
reason: HTTP2FrameUnhandledReason::ParsingError,
Expand All @@ -878,6 +929,22 @@ impl HTTP2State {
}
}
Some(parser::HTTP2FrameType::Headers) => {
if head.flags & parser::HTTP2_FLAG_HEADER_END_HEADERS == 0 {
let buf = if dir == Direction::ToClient {
&mut self.s2c_buf
} else {
&mut self.c2s_buf
};
buf.data.clear();
buf.data.extend(input);
buf.stream_id = head.stream_id;
let hs = parser::HTTP2FrameHeaders {
padlength: None,
priority: None,
blocks: Vec::new(),
};
return HTTP2FrameTypeData::HEADERS(hs);
}
let dyn_headers = if dir == Direction::ToClient {
&mut self.dynamic_headers_tc
} else {
Expand Down Expand Up @@ -961,15 +1028,19 @@ impl HTTP2State {
input = &rem[hlsafe..];
continue;
}
let mut reass_limit_reached = false;
let txdata = self.parse_frame_data(
head.ftype,
&head,
&rem[..hlsafe],
complete,
head.flags,
dir,
&mut reass_limit_reached,
);

let tx = self.find_or_create_tx(&head, &txdata, dir);
if reass_limit_reached {
tx.tx_data.set_event(HTTP2Event::ReassemblyLimitReached as u8);
}
tx.handle_frame(&head, &txdata, dir);
let over = head.flags & parser::HTTP2_FLAG_HEADER_EOS != 0;
let ftype = head.ftype;
Expand Down Expand Up @@ -1306,6 +1377,13 @@ pub unsafe extern "C" fn rs_http2_register_parser() {
SCLogError!("Invalid value for http2.max-table-size");
}
}
if let Some(val) = conf_get("app-layer.protocols.http2.max-reassembly-size") {
if let Ok(v) = val.parse::<u32>() {
HTTP2_MAX_REASS = v as usize;
} else {
SCLogError!("Invalid value for http2.max-reassembly-size");
}
}
SCLogDebug!("Rust http2 parser registered.");
} else {
SCLogNotice!("Protocol detector and parser disabled for HTTP2.");
Expand Down
2 changes: 2 additions & 0 deletions suricata.yaml.in
Original file line number Diff line number Diff line change
Expand Up @@ -934,6 +934,8 @@ app-layer:
#max-streams: 4096
# Maximum headers table size
#max-table-size: 65536
# Maximum reassembly size for header + continuation frames
#max-reassembly-size: 102400
smtp:
enabled: yes
raw-extraction: no
Expand Down

0 comments on commit 478a2a3

Please sign in to comment.