]> git.ipfire.org Git - thirdparty/suricata.git/commitdiff
http2: handle reassembly for continuation frames
authorPhilippe Antoine <pantoine@oisf.net>
Thu, 25 Jan 2024 15:01:14 +0000 (16:01 +0100)
committerVictor Julien <vjulien@oisf.net>
Wed, 7 Feb 2024 04:59:31 +0000 (05:59 +0100)
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 aff54f29f8c3f583ae0524a661aa90dc7a2d3f92)

rules/http2-events.rules
rust/src/http2/http2.rs
suricata.yaml.in

index a1d6bae7d358ee6d0fb441136c2b720cb35be2df..875a9af488a14e60d9a354dee8747cf7f3a15843 100644 (file)
@@ -17,3 +17,4 @@ alert http2 any any -> any any (msg:"SURICATA HTTP2 invalid HTTP1 settings durin
 alert http2 any any -> any any (msg:"SURICATA HTTP2 failed decompression"; flow:established; app-layer-event:http2.failed_decompression; classtype:protocol-command-decode; sid:2290009; 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;)
index 9df9488cdacb6953bcbe615fec6d04b94bd93fe6..bd871044f6eaeef41ff7356f047f839b293febc7 100644 (file)
@@ -20,6 +20,7 @@ use super::files::*;
 use super::decompression;
 use super::parser;
 use crate::applayer::{self, *};
+use crate::conf::conf_get;
 use crate::core::{
     self, AppProto, Flow, SuricataFileContext, ALPROTO_FAILED, ALPROTO_UNKNOWN, IPPROTO_TCP,
     STREAM_TOCLIENT, STREAM_TOSERVER,
@@ -64,6 +65,8 @@ const HTTP2_FRAME_PRIORITY_LEN: usize = 5;
 const HTTP2_FRAME_WINDOWUPDATE_LEN: usize = 4;
 //TODO make this configurable
 pub const HTTP2_MAX_TABLESIZE: u32 = 0x10000; // 65536
+// maximum size of reassembly for header + continuation
+static mut HTTP2_MAX_REASS: usize = 102400;
 
 #[repr(u8)]
 #[derive(Copy, Clone, PartialOrd, PartialEq, Debug)]
@@ -361,6 +364,7 @@ pub enum HTTP2Event {
     FailedDecompression,
     AuthorityHostMismatch,
     UserinfoInUri,
+    ReassemblyLimitReached,
 }
 
 impl HTTP2Event {
@@ -401,6 +405,12 @@ impl HTTP2DynTable {
     }
 }
 
+#[derive(Default)]
+struct HTTP2HeaderReassemblyBuffer {
+    data: Vec<u8>,
+    stream_id: u32,
+}
+
 pub struct HTTP2State {
     tx_id: u64,
     request_frame_size: u32,
@@ -410,6 +420,9 @@ pub struct HTTP2State {
     transactions: VecDeque<HTTP2Transaction>,
     progress: HTTP2ConnectionState,
     pub files: HTTP2Files,
+
+    c2s_buf: HTTP2HeaderReassemblyBuffer,
+    s2c_buf: HTTP2HeaderReassemblyBuffer,
 }
 
 impl HTTP2State {
@@ -426,6 +439,8 @@ impl HTTP2State {
             transactions: VecDeque::new(),
             progress: HTTP2ConnectionState::Http2StateInit,
             files: HTTP2Files::new(),
+            c2s_buf: HTTP2HeaderReassemblyBuffer::default(),
+            s2c_buf: HTTP2HeaderReassemblyBuffer::default(),
         }
     }
 
@@ -574,8 +589,11 @@ impl HTTP2State {
     }
 
     fn parse_frame_data(
-        &mut self, ftype: u8, input: &[u8], complete: bool, hflags: u8, dir: u8,
+        &mut self, head: &parser::HTTP2FrameHeader, input: &[u8], complete: bool, dir: u8,
+        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 {
@@ -735,17 +753,47 @@ impl HTTP2State {
                 return HTTP2FrameTypeData::DATA;
             }
             Some(parser::HTTP2FrameType::CONTINUATION) => {
+                let buf = if dir == STREAM_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 == STREAM_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(nom::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 {
@@ -758,6 +806,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,
@@ -766,6 +818,22 @@ impl HTTP2State {
                 }
             }
             Some(parser::HTTP2FrameType::HEADERS) => {
+                if head.flags & parser::HTTP2_FLAG_HEADER_END_HEADERS == 0 {
+                    let buf = if dir == STREAM_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 == STREAM_TOCLIENT {
                     &mut self.dynamic_headers_tc
                 } else {
@@ -847,15 +915,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.set_event(HTTP2Event::ReassemblyLimitReached);
+                    }
                     tx.handle_frame(&head, &txdata, dir);
                     let over = head.flags & parser::HTTP2_FLAG_HEADER_EOS != 0;
                     let ftype = head.ftype;
@@ -1206,6 +1278,7 @@ pub extern "C" fn rs_http2_state_get_event_info_by_id(
             HTTP2Event::FailedDecompression => "failed_decompression\0",
             HTTP2Event::AuthorityHostMismatch => "authority_host_mismatch\0",
             HTTP2Event::UserinfoInUri => "userinfo_in_uri\0",
+            HTTP2Event::ReassemblyLimitReached => "reassembly_limit_reached\0",
         };
         unsafe {
             *event_name = estr.as_ptr() as *const std::os::raw::c_char;
@@ -1292,6 +1365,13 @@ pub unsafe extern "C" fn rs_http2_register_parser() {
         if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 {
             let _ = AppLayerRegisterParser(&parser, alproto);
         }
+        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.");
index 5a2329e14dea44a985445638b35d4418e5344398..90d511e1a79a5b4ce797dfddfaef4b9881c81da6 100644 (file)
@@ -791,6 +791,8 @@ app-layer:
       enabled: no
       # use http keywords on HTTP2 traffic
       http1-rules: no
+      # Maximum reassembly size for header + continuation frames
+      #max-reassembly-size: 102400
     smtp:
       enabled: yes
       raw-extraction: no