diff --git a/rules/http-events.rules b/rules/http-events.rules index 7e4d1fd258..96a15d3f9d 100644 --- a/rules/http-events.rules +++ b/rules/http-events.rules @@ -97,4 +97,7 @@ alert http any any -> any any (msg:"SURICATA HTTP request too many headers"; flo alert http any any -> any any (msg:"SURICATA HTTP response too many headers"; flow:established,to_client; app-layer-event:http.response_too_many_headers; classtype:protocol-command-decode; sid:2221057; rev:1;) #alert http any any -> any any (msg:"SURICATA HTTP response chunk extension"; flow:established; app-layer-event:http.response_chunk_extension; classtype:protocol-command-decode; sid:2221058; rev:1;) -# next sid 2221059 + +alert http any any -> any any (msg:"SURICATA HTTP compression bomb limit reached"; flow:established; app-layer-event:http.compression_bomb_limit_reached; flowint:http.anomaly.count,+,1; classtype:protocol-command-decode; sid:2221059; rev:1;) + +# next sid 2221060 diff --git a/rust/htp/src/c_api/config.rs b/rust/htp/src/c_api/config.rs index e3c9110c77..f56238b47a 100644 --- a/rust/htp/src/c_api/config.rs +++ b/rust/htp/src/c_api/config.rs @@ -248,6 +248,20 @@ pub unsafe extern "C" fn htp_config_set_compression_bomb_limit( } } +/// Configures the maximum number of compression bombs LibHTP will decompress. +/// # Safety +/// When calling this method, you have to ensure that cfg is either properly initialized or NULL +#[no_mangle] +pub unsafe extern "C" fn htp_config_set_max_nb_compression_bombs( + cfg: *mut Config, max_bombs: libc::size_t, +) { + if let Ok(max_bombs) = max_bombs.try_into() { + if let Some(cfg) = cfg.as_mut() { + cfg.compression_options.set_max_bombs(max_bombs) + } + } +} + /// Configures the maximum compression time LibHTP will allow. /// # Safety /// When calling this method, you have to ensure that cfg is either properly initialized or NULL diff --git a/rust/htp/src/connection_parser.rs b/rust/htp/src/connection_parser.rs index 78f1bf099d..6e9f6119f1 100644 --- a/rust/htp/src/connection_parser.rs +++ b/rust/htp/src/connection_parser.rs @@ -352,6 +352,9 @@ pub struct ConnectionParser { /// The hook that should be receiving raw connection data. pub(crate) response_data_receiver_hook: Option, + /// Number of compression bombs seen. + pub(crate) bombs: u8, + /// Transactions processed by this parser transactions: Transactions, } @@ -402,6 +405,7 @@ impl ConnectionParser { response_state: State::Idle, response_state_previous: State::None, response_data_receiver_hook: None, + bombs: 0, transactions: Transactions::new(cfg, &logger), } } diff --git a/rust/htp/src/decompressors.rs b/rust/htp/src/decompressors.rs index 9df5b915e2..3d66b14a5b 100644 --- a/rust/htp/src/decompressors.rs +++ b/rust/htp/src/decompressors.rs @@ -21,6 +21,8 @@ const DEFAULT_TIME_LIMIT: u32 = 100_000; const DEFAULT_TIME_FREQ_TEST: u32 = 256; /// Default number of layers that will be decompressed const DEFAULT_LAYER_LIMIT: u32 = 2; +/// Default number of bombs before skipping decompression for the flow +const DEFAULT_BOMB_NB_LIMIT: u8 = 3; #[derive(Copy, Clone)] /// Decompression options @@ -31,6 +33,8 @@ pub(crate) struct Options { lzma_layers: Option, /// max output size for a compression bomb. bomb_limit: u64, + /// max number of compression bombs before skipping decompression for the flow + bomb_nb_limit: u8, /// max compressed-to-decrompressed ratio that should not be exceeded during decompression. bomb_ratio: u64, /// max time for a decompression bomb in microseconds. @@ -71,6 +75,16 @@ impl Options { self.bomb_limit } + /// Get the maximum compression bombs number. + pub(crate) fn get_max_bombs(&self) -> u8 { + self.bomb_nb_limit + } + + /// Set the maximum number of compression bombs before skipping decompression for the flow. + pub(crate) fn set_max_bombs(&mut self, max_bombs: u8) { + self.bomb_nb_limit = max_bombs; + } + /// Set the compression bomb limit. pub(crate) fn set_bomb_limit(&mut self, bomblimit: u64) { self.bomb_limit = bomblimit; @@ -122,6 +136,7 @@ impl Default for Options { }), lzma_layers: Some(DEFAULT_LZMA_LAYERS), bomb_limit: DEFAULT_BOMB_LIMIT, + bomb_nb_limit: DEFAULT_BOMB_NB_LIMIT, bomb_ratio: DEFAULT_BOMB_RATIO, time_limit: DEFAULT_TIME_LIMIT, time_test_freq: DEFAULT_TIME_FREQ_TEST, diff --git a/rust/htp/src/log.rs b/rust/htp/src/log.rs index 38ac21cb7f..7f4376cbee 100644 --- a/rust/htp/src/log.rs +++ b/rust/htp/src/log.rs @@ -110,6 +110,8 @@ pub enum HtpLogCode { LZMA_MEMLIMIT_REACHED, /// Reached configured time limit for decompression or reached bomb limit. COMPRESSION_BOMB, + /// Reached configured time limit for decompression or reached bomb limit. + COMPRESSION_BOMB_LIMIT_REACHED, /// Unexpected response body present. RESPONSE_BODY_UNEXPECTED, /// Content-length parsing contains extra leading characters. diff --git a/rust/htp/src/request.rs b/rust/htp/src/request.rs index 0f518e77ac..08e1c13a0c 100644 --- a/rust/htp/src/request.rs +++ b/rust/htp/src/request.rs @@ -1243,6 +1243,10 @@ impl ConnectionParser { /// Prepend a decompressor to the request fn request_prepend_decompressor(&mut self, encoding: HtpContentEncoding) -> Result<()> { let compression_options = self.cfg.compression_options; + if self.bombs >= compression_options.get_max_bombs() { + // skip decompression for this flow if too many bombs were seen + return Ok(()); + } if encoding != HtpContentEncoding::None { // ensured by caller let req = self.request_mut().unwrap(); @@ -1328,10 +1332,15 @@ impl ConnectionParser { request_entity_len, request_message_len, ) ); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "compression_bomb_limit reached", - )); + self.bombs += 1; + if self.bombs == compression_options.get_max_bombs() { + htp_error!( + self.logger, + HtpLogCode::COMPRESSION_BOMB_LIMIT_REACHED, + format!("Compression bomb: happened {} times", self.bombs,) + ); + } + return Err(std::io::Error::other("compression_bomb_limit reached")); } Ok(tx_data.len()) } diff --git a/rust/htp/src/response.rs b/rust/htp/src/response.rs index 604551b1fa..cc9540b9f8 100644 --- a/rust/htp/src/response.rs +++ b/rust/htp/src/response.rs @@ -1302,10 +1302,15 @@ impl ConnectionParser { response_entity_len, response_message_len, ) ); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "compression_bomb_limit reached", - )); + self.bombs += 1; + if self.bombs == compression_options.get_max_bombs() { + htp_error!( + self.logger, + HtpLogCode::COMPRESSION_BOMB_LIMIT_REACHED, + format!("Compression bomb: happened {} times", self.bombs,) + ); + } + return Err(std::io::Error::other("compression_bomb_limit reached")); } Ok(tx_data.len()) } @@ -1313,6 +1318,10 @@ impl ConnectionParser { /// Prepend response decompressor fn response_prepend_decompressor(&mut self, encoding: HtpContentEncoding) -> Result<()> { let compression_options = self.cfg.compression_options; + if self.bombs >= compression_options.get_max_bombs() { + // skip decompression for this flow if too many bombs were seen + return Ok(()); + } if encoding != HtpContentEncoding::None { // ensured by caller let resp = self.response_mut().unwrap(); diff --git a/src/app-layer-htp.c b/src/app-layer-htp.c index e2979f8dc5..88050bd299 100644 --- a/src/app-layer-htp.c +++ b/src/app-layer-htp.c @@ -202,6 +202,7 @@ SCEnumCharMap http_decoder_event_table[] = { { "LZMA_MEMLIMIT_REACHED", HTP_LOG_CODE_LZMA_MEMLIMIT_REACHED }, { "COMPRESSION_BOMB", HTP_LOG_CODE_COMPRESSION_BOMB }, + { "COMPRESSION_BOMB_LIMIT_REACHED", HTP_LOG_CODE_COMPRESSION_BOMB_LIMIT_REACHED }, { "REQUEST_TOO_MANY_HEADERS", HTP_LOG_CODE_REQUEST_TOO_MANY_HEADERS }, { "RESPONSE_TOO_MANY_HEADERS", HTP_LOG_CODE_RESPONSE_TOO_MANY_HEADERS }, @@ -2228,6 +2229,20 @@ static void HTPConfigParseParameters(HTPCfgRec *cfg_prec, SCConfNode *s, struct SCLogConfig("Setting HTTP LZMA decompression layers to %" PRIu32 "", (int)limit); htp_config_set_lzma_layers(cfg_prec->cfg, limit); } + } else if (strcasecmp("compression-bomb-count", p->name) == 0) { + uint8_t limit = 0; + if (ParseSizeStringU8(p->val, &limit) < 0) { + FatalError("failed to parse 'compression-bomb-count' " + "from conf file - %s.", + p->val); + } + if (limit == 0) { + FatalError("'compression-bomb-count' " + "from conf file cannot be 0."); + } + /* set default soft-limit with our new hard limit */ + SCLogConfig("Setting HTTP compression bomb count limit to %" PRIu8, limit); + htp_config_set_max_nb_compression_bombs(cfg_prec->cfg, (size_t)limit); } else if (strcasecmp("compression-bomb-limit", p->name) == 0) { uint32_t limit = 0; if (ParseSizeStringU32(p->val, &limit) < 0) { diff --git a/suricata.yaml.in b/suricata.yaml.in index aac7bf0363..63c00cf697 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -1182,6 +1182,10 @@ app-layer: # Maximum decompressed size with a compression ratio # above 2048 (only LZMA can reach this ratio, deflate cannot) #compression-bomb-limit: 1 MiB + # Maximum times in a single flow a compression bomb is allowed. + # If this is reached no more decompression will happen for the + # rest of that flow. + #compression-bomb-count: 3 # Maximum time spent decompressing a single transaction in usec #decompression-time-limit: 100000 # Maximum number of live transactions per flow