http1: limit the number of compression bombs per flow

Ticket: 8694

Otherwise, a flow full of small compression bombs is too slow
to process.

When the threshold is reached, decompression is skipped for the
rest of the flow.
pull/15814/head
Philippe Antoine 1 month ago committed by Victor Julien
parent bc41dcc854
commit 392b6aee29

@ -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

@ -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

@ -352,6 +352,9 @@ pub struct ConnectionParser {
/// The hook that should be receiving raw connection data.
pub(crate) response_data_receiver_hook: Option<DataHook>,
/// 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),
}
}

@ -22,6 +22,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
@ -32,6 +34,8 @@ pub(crate) struct Options {
lzma_layers: Option<u32>,
/// 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.
@ -72,6 +76,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;
@ -123,6 +137,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,

@ -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.

@ -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,6 +1332,14 @@ impl ConnectionParser {
request_entity_len, request_message_len,
)
);
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())

@ -1302,6 +1302,14 @@ impl ConnectionParser {
response_entity_len, response_message_len,
)
);
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())
@ -1310,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();

@ -196,6 +196,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 },
@ -2222,6 +2223,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) {

@ -1199,6 +1199,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

Loading…
Cancel
Save