Skip to content

Commit 392b6ae

Browse files
catenacybervictorjulien
authored andcommitted
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.
1 parent bc41dcc commit 392b6ae

9 files changed

Lines changed: 82 additions & 1 deletion

File tree

rules/http-events.rules

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,4 +97,7 @@ alert http any any -> any any (msg:"SURICATA HTTP request too many headers"; flo
9797
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;)
9898

9999
#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;)
100-
# next sid 2221059
100+
101+
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;)
102+
103+
# next sid 2221060

rust/htp/src/c_api/config.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,20 @@ pub unsafe extern "C" fn htp_config_set_compression_bomb_limit(
248248
}
249249
}
250250

251+
/// Configures the maximum number of compression bombs LibHTP will decompress.
252+
/// # Safety
253+
/// When calling this method, you have to ensure that cfg is either properly initialized or NULL
254+
#[no_mangle]
255+
pub unsafe extern "C" fn htp_config_set_max_nb_compression_bombs(
256+
cfg: *mut Config, max_bombs: libc::size_t,
257+
) {
258+
if let Ok(max_bombs) = max_bombs.try_into() {
259+
if let Some(cfg) = cfg.as_mut() {
260+
cfg.compression_options.set_max_bombs(max_bombs)
261+
}
262+
}
263+
}
264+
251265
/// Configures the maximum compression time LibHTP will allow.
252266
/// # Safety
253267
/// When calling this method, you have to ensure that cfg is either properly initialized or NULL

rust/htp/src/connection_parser.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,9 @@ pub struct ConnectionParser {
352352
/// The hook that should be receiving raw connection data.
353353
pub(crate) response_data_receiver_hook: Option<DataHook>,
354354

355+
/// Number of compression bombs seen.
356+
pub(crate) bombs: u8,
357+
355358
/// Transactions processed by this parser
356359
transactions: Transactions,
357360
}
@@ -402,6 +405,7 @@ impl ConnectionParser {
402405
response_state: State::Idle,
403406
response_state_previous: State::None,
404407
response_data_receiver_hook: None,
408+
bombs: 0,
405409
transactions: Transactions::new(cfg, &logger),
406410
}
407411
}

rust/htp/src/decompressors.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ const DEFAULT_TIME_LIMIT: u32 = 100_000;
2222
const DEFAULT_TIME_FREQ_TEST: u32 = 256;
2323
/// Default number of layers that will be decompressed
2424
const DEFAULT_LAYER_LIMIT: u32 = 2;
25+
/// Default number of bombs before skipping decompression for the flow
26+
const DEFAULT_BOMB_NB_LIMIT: u8 = 3;
2527

2628
#[derive(Copy, Clone)]
2729
/// Decompression options
@@ -32,6 +34,8 @@ pub(crate) struct Options {
3234
lzma_layers: Option<u32>,
3335
/// max output size for a compression bomb.
3436
bomb_limit: u64,
37+
/// max number of compression bombs before skipping decompression for the flow
38+
bomb_nb_limit: u8,
3539
/// max compressed-to-decrompressed ratio that should not be exceeded during decompression.
3640
bomb_ratio: u64,
3741
/// max time for a decompression bomb in microseconds.
@@ -72,6 +76,16 @@ impl Options {
7276
self.bomb_limit
7377
}
7478

79+
/// Get the maximum compression bombs number.
80+
pub(crate) fn get_max_bombs(&self) -> u8 {
81+
self.bomb_nb_limit
82+
}
83+
84+
/// Set the maximum number of compression bombs before skipping decompression for the flow.
85+
pub(crate) fn set_max_bombs(&mut self, max_bombs: u8) {
86+
self.bomb_nb_limit = max_bombs;
87+
}
88+
7589
/// Set the compression bomb limit.
7690
pub(crate) fn set_bomb_limit(&mut self, bomblimit: u64) {
7791
self.bomb_limit = bomblimit;
@@ -123,6 +137,7 @@ impl Default for Options {
123137
}),
124138
lzma_layers: Some(DEFAULT_LZMA_LAYERS),
125139
bomb_limit: DEFAULT_BOMB_LIMIT,
140+
bomb_nb_limit: DEFAULT_BOMB_NB_LIMIT,
126141
bomb_ratio: DEFAULT_BOMB_RATIO,
127142
time_limit: DEFAULT_TIME_LIMIT,
128143
time_test_freq: DEFAULT_TIME_FREQ_TEST,

rust/htp/src/log.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ pub enum HtpLogCode {
110110
LZMA_MEMLIMIT_REACHED,
111111
/// Reached configured time limit for decompression or reached bomb limit.
112112
COMPRESSION_BOMB,
113+
/// Reached configured time limit for decompression or reached bomb limit.
114+
COMPRESSION_BOMB_LIMIT_REACHED,
113115
/// Unexpected response body present.
114116
RESPONSE_BODY_UNEXPECTED,
115117
/// Content-length parsing contains extra leading characters.

rust/htp/src/request.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1243,6 +1243,10 @@ impl ConnectionParser {
12431243
/// Prepend a decompressor to the request
12441244
fn request_prepend_decompressor(&mut self, encoding: HtpContentEncoding) -> Result<()> {
12451245
let compression_options = self.cfg.compression_options;
1246+
if self.bombs >= compression_options.get_max_bombs() {
1247+
// skip decompression for this flow if too many bombs were seen
1248+
return Ok(());
1249+
}
12461250
if encoding != HtpContentEncoding::None {
12471251
// ensured by caller
12481252
let req = self.request_mut().unwrap();
@@ -1328,6 +1332,14 @@ impl ConnectionParser {
13281332
request_entity_len, request_message_len,
13291333
)
13301334
);
1335+
self.bombs += 1;
1336+
if self.bombs == compression_options.get_max_bombs() {
1337+
htp_error!(
1338+
self.logger,
1339+
HtpLogCode::COMPRESSION_BOMB_LIMIT_REACHED,
1340+
format!("Compression bomb: happened {} times", self.bombs,)
1341+
);
1342+
}
13311343
return Err(std::io::Error::other("compression_bomb_limit reached"));
13321344
}
13331345
Ok(tx_data.len())

rust/htp/src/response.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,14 @@ impl ConnectionParser {
13021302
response_entity_len, response_message_len,
13031303
)
13041304
);
1305+
self.bombs += 1;
1306+
if self.bombs == compression_options.get_max_bombs() {
1307+
htp_error!(
1308+
self.logger,
1309+
HtpLogCode::COMPRESSION_BOMB_LIMIT_REACHED,
1310+
format!("Compression bomb: happened {} times", self.bombs,)
1311+
);
1312+
}
13051313
return Err(std::io::Error::other("compression_bomb_limit reached"));
13061314
}
13071315
Ok(tx_data.len())
@@ -1310,6 +1318,10 @@ impl ConnectionParser {
13101318
/// Prepend response decompressor
13111319
fn response_prepend_decompressor(&mut self, encoding: HtpContentEncoding) -> Result<()> {
13121320
let compression_options = self.cfg.compression_options;
1321+
if self.bombs >= compression_options.get_max_bombs() {
1322+
// skip decompression for this flow if too many bombs were seen
1323+
return Ok(());
1324+
}
13131325
if encoding != HtpContentEncoding::None {
13141326
// ensured by caller
13151327
let resp = self.response_mut().unwrap();

src/app-layer-htp.c

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ SCEnumCharMap http_decoder_event_table[] = {
196196

197197
{ "LZMA_MEMLIMIT_REACHED", HTP_LOG_CODE_LZMA_MEMLIMIT_REACHED },
198198
{ "COMPRESSION_BOMB", HTP_LOG_CODE_COMPRESSION_BOMB },
199+
{ "COMPRESSION_BOMB_LIMIT_REACHED", HTP_LOG_CODE_COMPRESSION_BOMB_LIMIT_REACHED },
199200

200201
{ "REQUEST_TOO_MANY_HEADERS", HTP_LOG_CODE_REQUEST_TOO_MANY_HEADERS },
201202
{ "RESPONSE_TOO_MANY_HEADERS", HTP_LOG_CODE_RESPONSE_TOO_MANY_HEADERS },
@@ -2222,6 +2223,20 @@ static void HTPConfigParseParameters(HTPCfgRec *cfg_prec, SCConfNode *s, struct
22222223
SCLogConfig("Setting HTTP LZMA decompression layers to %" PRIu32 "", (int)limit);
22232224
htp_config_set_lzma_layers(cfg_prec->cfg, limit);
22242225
}
2226+
} else if (strcasecmp("compression-bomb-count", p->name) == 0) {
2227+
uint8_t limit = 0;
2228+
if (ParseSizeStringU8(p->val, &limit) < 0) {
2229+
FatalError("failed to parse 'compression-bomb-count' "
2230+
"from conf file - %s.",
2231+
p->val);
2232+
}
2233+
if (limit == 0) {
2234+
FatalError("'compression-bomb-count' "
2235+
"from conf file cannot be 0.");
2236+
}
2237+
/* set default soft-limit with our new hard limit */
2238+
SCLogConfig("Setting HTTP compression bomb count limit to %" PRIu8, limit);
2239+
htp_config_set_max_nb_compression_bombs(cfg_prec->cfg, (size_t)limit);
22252240
} else if (strcasecmp("compression-bomb-limit", p->name) == 0) {
22262241
uint32_t limit = 0;
22272242
if (ParseSizeStringU32(p->val, &limit) < 0) {

suricata.yaml.in

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1199,6 +1199,10 @@ app-layer:
11991199
# Maximum decompressed size with a compression ratio
12001200
# above 2048 (only LZMA can reach this ratio, deflate cannot)
12011201
#compression-bomb-limit: 1 MiB
1202+
# Maximum times in a single flow a compression bomb is allowed.
1203+
# If this is reached no more decompression will happen for the
1204+
# rest of that flow.
1205+
#compression-bomb-count: 3
12021206
# Maximum time spent decompressing a single transaction in usec
12031207
#decompression-time-limit: 100000
12041208
# Maximum number of live transactions per flow

0 commit comments

Comments
 (0)