From: Philippe Antoine Date: Mon, 19 Feb 2024 15:57:55 +0000 (+0100) Subject: ssh: limit length for banner logs X-Git-Tag: suricata-6.0.17~1 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b9963b3e291995a9936e938f28e3d94d82a10d0d;p=thirdparty%2Fsuricata.git ssh: limit length for banner logs Ticket: 6770 (cherry picked from commit c4b8fb7aca482d1a1555e27072ca26896b52a480) --- diff --git a/rust/src/jsonbuilder.rs b/rust/src/jsonbuilder.rs index 7f85692246..cf238d3f82 100644 --- a/rust/src/jsonbuilder.rs +++ b/rust/src/jsonbuilder.rs @@ -17,6 +17,7 @@ #![allow(clippy::missing_safety_doc)] +use std::collections::TryReserveError; use std::ffi::CStr; use std::os::raw::c_char; use std::str::Utf8Error; @@ -27,6 +28,7 @@ const INIT_SIZE: usize = 4096; pub enum JsonError { InvalidState, Utf8Error(Utf8Error), + Memory, } impl std::error::Error for JsonError {} @@ -36,10 +38,17 @@ impl std::fmt::Display for JsonError { match self { JsonError::InvalidState => write!(f, "invalid state"), JsonError::Utf8Error(ref e) => e.fmt(f), + JsonError::Memory => write!(f, "memory error"), } } } +impl From for JsonError { + fn from(_: TryReserveError) -> Self { + JsonError::Memory + } +} + impl From for JsonError { fn from(e: Utf8Error) -> Self { JsonError::Utf8Error(e) @@ -427,6 +436,22 @@ impl JsonBuilder { } } + /// Set a key and a string value (from bytes) on an object, with a limited size + pub fn set_string_from_bytes_limited(&mut self, key: &str, val: &[u8], limit: usize) -> Result<&mut Self, JsonError> { + let mut valtrunc = Vec::new(); + let val = if val.len() > limit { + valtrunc.extend_from_slice(&val[..limit]); + valtrunc.extend_from_slice(b"[truncated]"); + &valtrunc + } else { + val + }; + match std::str::from_utf8(val) { + Ok(s) => self.set_string(key, s), + Err(_) => self.set_string(key, &try_string_from_bytes(val)?), + } + } + /// Set a key and an unsigned integer type on an object. pub fn set_uint(&mut self, key: &str, val: u64) -> Result<&mut Self, JsonError> { match self.current_state() { @@ -570,6 +595,26 @@ fn string_from_bytes(input: &[u8]) -> String { return out; } +/// A Suricata specific function to create a string from bytes when UTF-8 decoding fails. +/// +/// For bytes over 0x0f, we encode as hex like "\xf2". +fn try_string_from_bytes(input: &[u8]) -> Result { + let mut out = String::new(); + + // Allocate enough data to handle the worst case scenario of every + // byte needing to be presented as a byte. + out.try_reserve(input.len() * 4)?; + + for b in input.iter() { + if *b < 128 { + out.push(*b as char); + } else { + out.push_str(&format!("\\x{:02x}", *b)); + } + } + return Ok(out); +} + #[no_mangle] pub extern "C" fn jb_new_object() -> *mut JsonBuilder { let boxed = Box::new(JsonBuilder::new_object()); diff --git a/rust/src/ssh/logger.rs b/rust/src/ssh/logger.rs index 7f59651463..7af0ed80cc 100644 --- a/rust/src/ssh/logger.rs +++ b/rust/src/ssh/logger.rs @@ -15,7 +15,7 @@ * 02110-1301, USA. */ -use super::ssh::SSHTransaction; +use super::ssh::{SSHTransaction, SSH_MAX_BANNER_LEN}; use crate::jsonbuilder::{JsonBuilder, JsonError}; fn log_ssh(tx: &SSHTransaction, js: &mut JsonBuilder) -> Result { @@ -24,9 +24,9 @@ fn log_ssh(tx: &SSHTransaction, js: &mut JsonBuilder) -> Result } if tx.cli_hdr.protover.len() > 0 { js.open_object("client")?; - js.set_string_from_bytes("proto_version", &tx.cli_hdr.protover)?; - if tx.cli_hdr.swver.len() > 0 { - js.set_string_from_bytes("software_version", &tx.cli_hdr.swver)?; + js.set_string_from_bytes_limited("proto_version", &tx.cli_hdr.protover, SSH_MAX_BANNER_LEN)?; + if !tx.cli_hdr.swver.is_empty() { + js.set_string_from_bytes_limited("software_version", &tx.cli_hdr.swver, SSH_MAX_BANNER_LEN)?; } if tx.cli_hdr.hassh.len() > 0 || tx.cli_hdr.hassh_string.len() > 0 { js.open_object("hassh")?; @@ -42,9 +42,9 @@ fn log_ssh(tx: &SSHTransaction, js: &mut JsonBuilder) -> Result } if tx.srv_hdr.protover.len() > 0 { js.open_object("server")?; - js.set_string_from_bytes("proto_version", &tx.srv_hdr.protover)?; - if tx.srv_hdr.swver.len() > 0 { - js.set_string_from_bytes("software_version", &tx.srv_hdr.swver)?; + js.set_string_from_bytes_limited("proto_version", &tx.srv_hdr.protover, SSH_MAX_BANNER_LEN)?; + if !tx.srv_hdr.swver.is_empty() { + js.set_string_from_bytes_limited("software_version", &tx.srv_hdr.swver, SSH_MAX_BANNER_LEN)?; } if tx.srv_hdr.hassh.len() > 0 || tx.srv_hdr.hassh_string.len() > 0 { js.open_object("hassh")?; diff --git a/rust/src/ssh/ssh.rs b/rust/src/ssh/ssh.rs index ec8368d7ce..b57fbf3fab 100644 --- a/rust/src/ssh/ssh.rs +++ b/rust/src/ssh/ssh.rs @@ -59,7 +59,7 @@ pub enum SSHConnectionState { SshStateFinished = 3, } -const SSH_MAX_BANNER_LEN: usize = 256; +pub const SSH_MAX_BANNER_LEN: usize = 256; const SSH_RECORD_HEADER_LEN: usize = 6; const SSH_MAX_REASSEMBLED_RECORD_LEN: usize = 65535;