http2: protection against decompression bombs

Ticket: 8513

During decompression, fail early if we have a big decompression
ratio, and enough data.
Track this data also during a tx lifetime, and even a flow/state
lifetime, so that we set event and fail also if the compression
bomb is split over multiple packets
pull/15328/merge
Philippe Antoine 2 months ago committed by Victor Julien
parent 29e4b08647
commit 7bf48b02be

@ -26,3 +26,4 @@ alert http2 any any -> any any (msg:"SURICATA HTTP2 dns response too long"; flow
alert http2 any any -> any any (msg:"SURICATA HTTP2 data on stream zero"; flow:established; app-layer-event:http2.data_stream_zero; classtype:protocol-command-decode; sid:2290018; rev:1;)
# disabled by default, as it can happen in legit cases depending on the max-frames config value
# alert http2 any any -> any any (msg:"SURICATA HTTP2 too many frames"; flow:established; app-layer-event:http2.too_many_frames; classtype:protocol-command-decode; sid:2290019; rev:1;)
alert http2 any any -> any any (msg:"SURICATA HTTP2 compression bomb"; flow:established; app-layer-event:http2.compression_bomb; classtype:protocol-command-decode; sid:2290020; rev:1;)

@ -15,6 +15,7 @@
* 02110-1301, USA.
*/
use super::http2::HTTP2_COMPRESSION_BOMB_LIMIT;
use crate::direction::Direction;
use brotli;
use flate2::read::{DeflateDecoder, GzDecoder};
@ -24,6 +25,8 @@ use std::io::{Cursor, Read, Write};
pub const HTTP2_DECOMPRESSION_CHUNK_SIZE: usize = 0x1000; // 4096
pub(super) const DEFAULT_BOMB_RATIO: u64 = 2048;
#[repr(u8)]
#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Debug)]
pub enum HTTP2ContentEncoding {
@ -102,9 +105,11 @@ impl std::fmt::Debug for HTTP2Decompresser {
}
#[derive(Debug)]
struct HTTP2DecoderHalf {
pub(super) struct HTTP2DecoderHalf {
encoding: HTTP2ContentEncoding,
decoder: HTTP2Decompresser,
pub input_len: u64,
pub output_len: u64,
}
pub trait GetMutCursor {
@ -141,6 +146,7 @@ fn http2_decompress<'a>(
let mut offset = 0;
decoder.get_mut().set_position(0);
output.resize(HTTP2_DECOMPRESSION_CHUNK_SIZE, 0);
let max_len = DEFAULT_BOMB_RATIO * input.len() as u64;
loop {
match decoder.read(&mut output[offset..]) {
Ok(0) => {
@ -149,6 +155,14 @@ fn http2_decompress<'a>(
Ok(n) => {
offset += n;
if offset == output.len() {
if output.len() + HTTP2_DECOMPRESSION_CHUNK_SIZE > max_len as usize
&& output.len() > unsafe { HTTP2_COMPRESSION_BOMB_LIMIT as usize }
{
return Err(io::Error::new(
io::ErrorKind::OutOfMemory,
"Decompression bomb detected",
));
}
output.resize(output.len() + HTTP2_DECOMPRESSION_CHUNK_SIZE, 0);
}
}
@ -170,6 +184,8 @@ impl HTTP2DecoderHalf {
HTTP2DecoderHalf {
encoding: HTTP2ContentEncoding::Unknown,
decoder: HTTP2Decompresser::Unassigned,
input_len: 0,
output_len: 0,
}
}
@ -202,23 +218,41 @@ impl HTTP2DecoderHalf {
match self.decoder {
HTTP2Decompresser::Gzip(ref mut gzip_decoder) => {
let r = http2_decompress(&mut *gzip_decoder.as_mut(), input, output);
if r.is_err() {
self.decoder = HTTP2Decompresser::Unassigned;
match r {
Err(_) => {
self.decoder = HTTP2Decompresser::Unassigned;
}
Ok(o) => {
self.output_len += o.len() as u64;
}
}
self.input_len += input.len() as u64;
return r;
}
HTTP2Decompresser::Brotli(ref mut br_decoder) => {
let r = http2_decompress(&mut *br_decoder.as_mut(), input, output);
if r.is_err() {
self.decoder = HTTP2Decompresser::Unassigned;
match r {
Err(_) => {
self.decoder = HTTP2Decompresser::Unassigned;
}
Ok(o) => {
self.output_len += o.len() as u64;
}
}
self.input_len += input.len() as u64;
return r;
}
HTTP2Decompresser::Deflate(ref mut df_decoder) => {
let r = http2_decompress(&mut *df_decoder.as_mut(), input, output);
if r.is_err() {
self.decoder = HTTP2Decompresser::Unassigned;
match r {
Err(_) => {
self.decoder = HTTP2Decompresser::Unassigned;
}
Ok(o) => {
self.output_len += o.len() as u64;
}
}
self.input_len += input.len() as u64;
return r;
}
_ => {}
@ -228,9 +262,9 @@ impl HTTP2DecoderHalf {
}
#[derive(Debug)]
pub struct HTTP2Decoder {
decoder_tc: HTTP2DecoderHalf,
decoder_ts: HTTP2DecoderHalf,
pub(super) struct HTTP2Decoder {
pub decoder_tc: HTTP2DecoderHalf,
pub decoder_ts: HTTP2DecoderHalf,
}
impl HTTP2Decoder {

@ -22,7 +22,7 @@ use super::range;
use super::range::{SCHTPFileCloseHandleRange, SCHttpRangeFreeBlock};
use crate::applayer::{self, *};
use crate::conf::conf_get;
use crate::conf::{conf_get, get_memval};
use crate::core::*;
use crate::direction::Direction;
use crate::dns::dns::DnsVariant;
@ -81,6 +81,7 @@ pub static mut HTTP2_MAX_TABLESIZE: u32 = 65536; // 0x10000
static mut HTTP2_MAX_REASS: usize = 102400;
static mut HTTP2_MAX_STREAMS: usize = 4096; // 0x1000
static mut HTTP2_MAX_FRAMES: usize = 65536;
pub(super) static mut HTTP2_COMPRESSION_BOMB_LIMIT: u64 = 1_048_576;
#[derive(AppLayerFrameType)]
pub enum Http2FrameType {
@ -519,8 +520,12 @@ impl HTTP2Transaction {
self.handle_dns_data(dir, flow);
}
}
_ => {
self.set_event(HTTP2Event::FailedDecompression);
Err(e) => {
if e.kind() == io::ErrorKind::OutOfMemory {
self.set_event(HTTP2Event::CompressionBomb);
} else {
self.set_event(HTTP2Event::FailedDecompression);
}
}
}
}
@ -561,6 +566,7 @@ pub enum HTTP2Event {
DnsResponseTooLong,
DataStreamZero,
TooManyFrames,
CompressionBomb,
}
pub struct HTTP2DynTable {
@ -603,6 +609,9 @@ pub struct HTTP2State {
transactions: VecDeque<HTTP2Transaction>,
progress: HTTP2ConnectionState,
comp_len: u64,
decomp_len: u64,
c2s_buf: HTTP2HeaderReassemblyBuffer,
s2c_buf: HTTP2HeaderReassemblyBuffer,
}
@ -637,6 +646,8 @@ impl HTTP2State {
dynamic_headers_tc: HTTP2DynTable::new(),
transactions: VecDeque::new(),
progress: HTTP2ConnectionState::Http2StateInit,
comp_len: 0,
decomp_len: 0,
c2s_buf: HTTP2HeaderReassemblyBuffer::default(),
s2c_buf: HTTP2HeaderReassemblyBuffer::default(),
}
@ -1234,6 +1245,7 @@ impl HTTP2State {
&mut reass_limit_reached,
);
let (comp_len, decomp_len) = (self.comp_len, self.decomp_len);
let tx = self.find_or_create_tx(&head, &txdata, dir);
if tx.is_none() {
return AppLayerResult::err();
@ -1290,6 +1302,28 @@ impl HTTP2State {
tx.tx_data.set_event(HTTP2Event::DataStreamZero as u8);
} else if ftype == parser::HTTP2FrameType::Data as u8 && sid > 0 {
tx.handle_data_frame(rem, hlsafe, dir, flow, padded, over);
let (il, ol) = if dir == Direction::ToClient {
(
tx.decoder.decoder_tc.input_len,
tx.decoder.decoder_tc.output_len,
)
} else {
(
tx.decoder.decoder_ts.input_len,
tx.decoder.decoder_ts.output_len,
)
};
let (il, ol) = (il + comp_len, ol + decomp_len);
if ol > decompression::DEFAULT_BOMB_RATIO * il {
if ol > unsafe { HTTP2_COMPRESSION_BOMB_LIMIT } {
tx.set_event(HTTP2Event::CompressionBomb);
return AppLayerResult::err();
}
if over {
self.comp_len += il;
self.decomp_len += ol;
}
}
}
sc_app_layer_parser_trigger_raw_stream_inspection(flow, dir as i32);
input = &rem[hlsafe..];
@ -1600,6 +1634,13 @@ pub unsafe extern "C" fn SCRegisterHttp2Parser() {
SCLogError!("Invalid value for http2.max-reassembly-size");
}
}
if let Some(val) = conf_get("app-layer.protocols.http2.compression-bomb-limit") {
if let Ok(v) = get_memval(val) {
HTTP2_COMPRESSION_BOMB_LIMIT = v;
} else {
SCLogWarning!("Invalid value for http2.compression-bomb-limit");
}
}
SCAppLayerParserRegisterLogger(IPPROTO_TCP, ALPROTO_HTTP2);
SCLogDebug!("Rust http2 parser registered.");
} else {

@ -1032,6 +1032,8 @@ app-layer:
#max-reassembly-size: 102400
# Maximum number of frames per tx
#max-frames: 65536
# Maximum data to decompress if the decompress ratio is too high
#compression-bomb-limit: 1MiB
smtp:
enabled: yes
raw-extraction: no

Loading…
Cancel
Save