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.

(cherry picked from commit 392b6aee29)
pull/15823/head
Philippe Antoine 2 months ago committed by Victor Julien
parent 26c26dea84
commit e369bf29ee

@ -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),
}
}

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

@ -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,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())
}

@ -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();

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

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

Loading…
Cancel
Save