From f839581dba87f8c30e122868a8235851b3a8a158 Mon Sep 17 00:00:00 2001 From: Juliana Fajardini Date: Wed, 26 Aug 2026 16:59:35 -0300 Subject: [PATCH] pgsql: bound tx.responses (configurable) Since the PostgreSQL protocol doesn't impose any clear limits to how many responses a backend could send, add one to the app-proto parser. Using 21-64 as the accepted values, and 64 as the default: The Start-up phase may have up to 17-18 ParameterStatus messages, plus AuthOk, BackendKeyData and ReadyForQuery: 20, with extra room to be on the safer side: 21 And 64 as upper limit, because as we currently summarize query responses, we already don't get flooded by DataRow and Copy responses messages, so we don't expect to see that many responses in a single transaction for the pgsql, with the current implementation. Ticket: #7263 --- rules/pgsql-events.rules | 1 + rust/src/pgsql/pgsql.rs | 67 +++++++++++++++++++++++++++++++++------- suricata.yaml.in | 4 +++ 3 files changed, 61 insertions(+), 11 deletions(-) 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