commit 26df74be5f3c86f800df9ee2cf23035dccf852dc
parent 6c741c3d69a4545c6e608f687e96f995f71a9d81
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Mar 2026 01:58:56 +0000
simplex: validate proxy server transport hosts
Diffstat:
1 file changed, 139 insertions(+), 2 deletions(-)
diff --git a/crates/simplex-smp-proto/src/wire.rs b/crates/simplex-smp-proto/src/wire.rs
@@ -11,6 +11,8 @@ use crate::version::{
use alloc::boxed::Box;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
+use core::net::Ipv6Addr;
+use core::str::FromStr;
const TAG_NEW: &[u8] = b"NEW";
const TAG_SUB: &[u8] = b"SUB";
@@ -1106,6 +1108,7 @@ fn encode_protocol_server(
buffer: &mut Vec<u8>,
server: &RadrootsSimplexSmpProtocolServer,
) -> Result<(), RadrootsSimplexSmpProtoError> {
+ validate_transport_hosts(&server.hosts)?;
push_short_string_list(buffer, &server.hosts)?;
push_short_string(buffer, &server.port)?;
push_short_bytes(buffer, &server.key_hash)
@@ -1114,8 +1117,10 @@ fn encode_protocol_server(
fn decode_protocol_server(
cursor: &mut Cursor<'_>,
) -> Result<RadrootsSimplexSmpProtocolServer, RadrootsSimplexSmpProtoError> {
+ let hosts = cursor.read_short_string_list()?;
+ validate_transport_hosts(&hosts)?;
Ok(RadrootsSimplexSmpProtocolServer {
- hosts: cursor.read_short_string_list()?,
+ hosts,
port: cursor.read_short_string_lossy()?,
key_hash: cursor.read_short_bytes()?,
})
@@ -1212,6 +1217,83 @@ fn decode_queue_mode(
}
}
+fn validate_transport_hosts(hosts: &[String]) -> Result<(), RadrootsSimplexSmpProtoError> {
+ for host in hosts {
+ validate_transport_host(host)?;
+ }
+ Ok(())
+}
+
+fn validate_transport_host(host: &str) -> Result<(), RadrootsSimplexSmpProtoError> {
+ if is_valid_ipv4_transport_host(host)
+ || is_valid_ipv6_transport_host(host)
+ || is_valid_onion_transport_host(host)
+ || is_valid_domain_transport_host(host)
+ {
+ return Ok(());
+ }
+ Err(RadrootsSimplexSmpProtoError::InvalidHostList(
+ host.to_string(),
+ ))
+}
+
+fn is_valid_ipv4_transport_host(host: &str) -> bool {
+ let mut segments = 0_usize;
+ for segment in host.split('.') {
+ if segment.is_empty() || !segment.bytes().all(|byte| byte.is_ascii_digit()) {
+ return false;
+ }
+ if segment.parse::<u16>().map_or(true, |value| value > 255) {
+ return false;
+ }
+ segments += 1;
+ }
+ segments == 4
+}
+
+fn is_valid_ipv6_transport_host(host: &str) -> bool {
+ let candidate = if let Some(stripped) = host.strip_prefix('[') {
+ let Some(inner) = stripped.strip_suffix(']') else {
+ return false;
+ };
+ inner
+ } else {
+ if host.ends_with(']') {
+ return false;
+ }
+ host
+ };
+ if candidate.is_empty()
+ || !candidate
+ .bytes()
+ .all(|byte| byte.is_ascii_hexdigit() || byte == b':')
+ {
+ return false;
+ }
+ Ipv6Addr::from_str(candidate).is_ok()
+}
+
+fn is_valid_onion_transport_host(host: &str) -> bool {
+ let Some(prefix) = host.strip_suffix(".onion") else {
+ return false;
+ };
+ !prefix.is_empty()
+ && prefix
+ .bytes()
+ .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
+}
+
+fn is_valid_domain_transport_host(host: &str) -> bool {
+ !host.is_empty()
+ && !host.ends_with(".onion")
+ && !host.starts_with('[')
+ && !host.ends_with(']')
+ && !host.contains(':')
+ && host
+ .chars()
+ .all(|character| !matches!(character, '#' | ',' | ';' | '/' | ' ' | '\n' | '\r' | '\t'))
+}
+
fn encode_transmission(
authorization: &[u8],
correlation_id: Option<RadrootsSimplexSmpCorrelationId>,
@@ -1836,6 +1918,11 @@ impl<'a> Cursor<'a> {
Ok(self.read_exact(len)?.to_vec())
}
+ fn read_short_string(&mut self) -> Result<String, RadrootsSimplexSmpProtoError> {
+ String::from_utf8(self.read_short_bytes()?)
+ .map_err(|error| RadrootsSimplexSmpProtoError::InvalidUtf8(error.to_string()))
+ }
+
fn read_short_string_lossy(&mut self) -> Result<String, RadrootsSimplexSmpProtoError> {
Ok(String::from_utf8_lossy(&self.read_short_bytes()?).into_owned())
}
@@ -1862,7 +1949,7 @@ impl<'a> Cursor<'a> {
}
let mut values = Vec::with_capacity(len);
for _ in 0..len {
- values.push(self.read_short_string_lossy()?);
+ values.push(self.read_short_string()?);
}
Ok(values)
}
@@ -2411,6 +2498,56 @@ mod tests {
}
#[test]
+ fn protocol_server_accepts_official_transport_host_forms() {
+ let server = RadrootsSimplexSmpProtocolServer {
+ hosts: vec![
+ "smp4.simplex.im".to_string(),
+ "192.0.2.24".to_string(),
+ "2001:db8::24".to_string(),
+ "[2001:db8::42]".to_string(),
+ "simplexabc.onion".to_string(),
+ ],
+ port: "5223".to_string(),
+ key_hash: vec![0xaa, 0xbb, 0xcc],
+ };
+
+ let mut encoded = Vec::new();
+ encode_protocol_server(&mut encoded, &server).unwrap();
+ let decoded = decode_protocol_server(&mut Cursor::new(&encoded)).unwrap();
+
+ assert_eq!(decoded, server);
+ }
+
+ #[test]
+ fn protocol_server_rejects_invalid_transport_host_forms() {
+ let invalid_server = RadrootsSimplexSmpProtocolServer {
+ hosts: vec!["bad host".to_string()],
+ port: "5223".to_string(),
+ key_hash: vec![0xaa, 0xbb, 0xcc],
+ };
+
+ let mut encoded = Vec::new();
+ assert_eq!(
+ encode_protocol_server(&mut encoded, &invalid_server),
+ Err(RadrootsSimplexSmpProtoError::InvalidHostList(
+ "bad host".to_string(),
+ ))
+ );
+
+ let mut invalid_bytes = Vec::new();
+ push_short_string_list(&mut invalid_bytes, &["[invalid]".to_string()]).unwrap();
+ push_short_string(&mut invalid_bytes, "5223").unwrap();
+ push_short_bytes(&mut invalid_bytes, &[0xaa, 0xbb, 0xcc]).unwrap();
+
+ assert_eq!(
+ decode_protocol_server(&mut Cursor::new(&invalid_bytes)),
+ Err(RadrootsSimplexSmpProtoError::InvalidHostList(
+ "[invalid]".to_string(),
+ ))
+ );
+ }
+
+ #[test]
fn round_trips_proxy_forward_commands() {
let pfwd = RadrootsSimplexSmpCommandTransmission {
authorization: Vec::new(),