diff --git a/rules/pgsql-events.rules b/rules/pgsql-events.rules index c19332033d..b0009b659d 100644 --- a/rules/pgsql-events.rules +++ b/rules/pgsql-events.rules @@ -6,3 +6,4 @@ alert pgsql any any -> any any (msg:"SURICATA PGSQL Too many transactions"; app- alert pgsql any any -> any any (msg:"SURICATA PGSQL Malformed request"; app-layer-event:pgsql.malformed_request; flow:to_server; sid:2241001; rev:1;) alert pgsql any any -> any any (msg:"SURICATA PGSQL Malformed response"; app-layer-event:pgsql.malformed_response; flow:to_client; sid:2241002; rev:1;) alert pgsql any any -> any any (msg:"SURICATA PGSQL Invalid length"; app-layer-event:pgsql.invalid_length; sid:2241003; rev:1;) +alert pgsql any any -> any any (msg:"SURICATA PGSQL Too Many Responses in a single transaction"; app-layer-event:pgsql.too_many_responses; flow:to_client; sid:2241004; rev:1;) diff --git a/rust/src/pgsql/pgsql.rs b/rust/src/pgsql/pgsql.rs index b01fd4b748..cc1ee6db73 100644 --- a/rust/src/pgsql/pgsql.rs +++ b/rust/src/pgsql/pgsql.rs @@ -37,10 +37,15 @@ use suricata_sys::sys::{ }; const PGSQL_CONFIG_DEFAULT_STREAM_DEPTH: u32 = 0; +// Min: 21 for the longest set of responses in one tx, for this parser: +// AuthOk + ParameterStatus (var, max 17-18) + BackendKeyData + ReadyForQuery +const PGSQL_CONFIG_DEFAULT_MIN_RESPONSES: usize = 21; +const PGSQL_CONFIG_DEFAULT_MAX_RESPONSES: usize = 64; pub(crate) static mut ALPROTO_PGSQL: AppProto = ALPROTO_UNKNOWN; static mut PGSQL_MAX_TX: usize = 1024; +static mut PGSQL_MAX_RESPONSES: usize = PGSQL_CONFIG_DEFAULT_MAX_RESPONSES; #[derive(AppLayerEvent, Debug, PartialEq, Eq)] enum PgsqlEvent { @@ -48,6 +53,7 @@ enum PgsqlEvent { MalformedRequest, // Enough data, but unexpected request format MalformedResponse, // Enough data, but unexpected response format TooManyTransactions, + TooManyResponses, } #[repr(u8)] @@ -70,6 +76,7 @@ pub(crate) struct PgsqlTransaction { pub data_row_cnt: u64, pub data_size: u64, + is_max_responses_set: bool, tx_data: AppLayerTxData, } @@ -95,6 +102,7 @@ impl PgsqlTransaction { responses: Vec::::new(), data_row_cnt: 0, data_size: 0, + is_max_responses_set: false, tx_data: AppLayerTxData::new(), } } @@ -656,11 +664,17 @@ impl PgsqlState { row_cnt: tx.get_row_cnt(), data_size: tx.data_size, // total byte count of all data_row messages combined }); - tx.responses.push(consolidated_data_row); - tx.responses.push(response); - // reset values - tx.data_row_cnt = 0; - tx.data_size = 0; + if tx.responses.len() + 2 <= unsafe { PGSQL_MAX_RESPONSES } { + // we need two vacant places here + tx.responses.push(consolidated_data_row); + tx.responses.push(response); + // reset values + tx.data_row_cnt = 0; + tx.data_size = 0; + } else if !tx.is_max_responses_set { + tx.tx_data.set_event(PgsqlEvent::TooManyResponses as u8); + tx.is_max_responses_set = true; + } } else if matches!(response, PgsqlBEMessage::ConsolidatedCopyDataOut(_)) { tx.incr_row_cnt(); } else if matches!(response, PgsqlBEMessage::CopyDone(_)) @@ -675,11 +689,17 @@ impl PgsqlState { data_size: tx.data_size, // total byte count of all data_row messages combined }, ); - tx.responses.push(consolidated_copy_data); - tx.responses.push(response); - // reset values - tx.data_row_cnt = 0; - tx.data_size = 0; + if tx.responses.len() + 2 <= unsafe { PGSQL_MAX_RESPONSES } { + // we need two vacant spaces, here + tx.responses.push(consolidated_copy_data); + tx.responses.push(response); + // reset values + tx.data_row_cnt = 0; + tx.data_size = 0; + } else if !tx.is_max_responses_set { + tx.tx_data.set_event(PgsqlEvent::TooManyResponses as u8); + tx.is_max_responses_set = true; + } } else { if response.is_malformed() { tx.tx_data.set_event(PgsqlEvent::MalformedResponse as u8); @@ -695,7 +715,12 @@ impl PgsqlState { // vector, either // Don't log NoticeResponse, NotificationResponse, nor AuthenticationSSPI messages, // either (temporarily) - tx.responses.push(response); + if tx.responses.len() < unsafe { PGSQL_MAX_RESPONSES } { + tx.responses.push(response); + } else if !tx.is_max_responses_set { + tx.tx_data.set_event(PgsqlEvent::TooManyResponses as u8); + tx.is_max_responses_set = true; + } } if let Some(state) = new_state { @@ -1012,6 +1037,26 @@ pub unsafe extern "C" fn SCRegisterPgsqlParser() { SCLogError!("Invalid value for pgsql.max-tx"); } } + if let Some(val) = conf_get("app-layer.protocols.pgsql.max-responses") { + if let Ok(v) = val.parse::() { + if (PGSQL_CONFIG_DEFAULT_MIN_RESPONSES..=PGSQL_CONFIG_DEFAULT_MAX_RESPONSES) + .contains(&v) + { + PGSQL_MAX_RESPONSES = v; + } else { + SCLogWarning!( + "Invalid value of {} for pgsql.max-responses, keeping default of {}", + v, + PGSQL_CONFIG_DEFAULT_MAX_RESPONSES + ); + } + } else { + SCLogWarning!( + "Invalid value for pgsql.max-responses, keeping default of {}", + PGSQL_CONFIG_DEFAULT_MAX_RESPONSES + ); + } + } } else { SCLogDebug!("Protocol detector and parser disabled for PGSQL."); } diff --git a/suricata.yaml.in b/suricata.yaml.in index 4673c9b8a3..6ada008b8a 100644 --- a/suricata.yaml.in +++ b/suricata.yaml.in @@ -995,6 +995,10 @@ app-layer: stream-depth: 0 # Maximum number of live PostgreSQL transactions per flow # max-tx: 1024 + # Maximum number of responses per PGSQL transaction + # Accepted values: 21-64, (min 21 to accommodate a full Startup sequence + # of responses) + # max-responses: 64 dcerpc: enabled: yes # Maximum number of live DCERPC transactions per flow