fix(security): enforce outbound connection policy (#5135)

Co-authored-by: cxymds <cxymds@gmail.com>
This commit is contained in:
Zhengchao An
2026-07-23 06:11:28 +08:00
committed by GitHub
parent 6e88ab2a25
commit c9848a6096
10 changed files with 1366 additions and 104 deletions
+14 -4
View File
@@ -23,7 +23,7 @@ use rustfs_config::{
NATS_TLS_CLIENT_KEY, NATS_TOKEN, NATS_USERNAME, PULSAR_AUTH_TOKEN, PULSAR_PASSWORD, PULSAR_QUEUE_DIR, PULSAR_TLS_CA,
PULSAR_TOPIC, PULSAR_USERNAME,
};
use rustfs_utils::egress::validate_outbound_url;
use rustfs_utils::egress::OutboundPolicy;
use std::collections::HashSet;
use std::path::Path;
use std::str::FromStr;
@@ -215,16 +215,20 @@ pub(super) fn validate_pulsar_broker_config(broker: &str, config: &KVS, default_
}
pub(super) fn parse_url(value: &str, field_label: &str) -> Result<Url, TargetError> {
Url::parse(value).map_err(|e| TargetError::Configuration(format!("Invalid {field_label}: {e} (value: '{value}')")))
Url::parse(value).map_err(|e| TargetError::Configuration(format!("Invalid {field_label}: {e}")))
}
pub(super) fn validate_outbound_http_url(value: &Url, field_label: &str) -> Result<(), TargetError> {
validate_outbound_url(value).map_err(|e| TargetError::Configuration(format!("{field_label} is not allowed: {e}")))
let policy =
OutboundPolicy::from_env_cached().map_err(|err| TargetError::Configuration(format!("invalid outbound policy: {err}")))?;
policy
.validate_url(value)
.map_err(|e| TargetError::Configuration(format!("{field_label} is not allowed: {e}")))
}
#[cfg(test)]
mod tests {
use super::{parse_jetstream_enable, validate_nats_server_config, validate_pulsar_broker_config};
use super::{parse_jetstream_enable, parse_url, validate_nats_server_config, validate_pulsar_broker_config};
use async_nats::ServerAddr;
use rustfs_config::server_config::KVS;
use rustfs_config::{
@@ -238,6 +242,12 @@ mod tests {
ServerAddr::from_str("nats://127.0.0.1:4222").expect("valid nats address")
}
#[test]
fn parse_url_error_does_not_echo_the_configured_value() {
let err = parse_url("not a URL containing secret-token", "endpoint URL").expect_err("invalid URL should fail");
assert!(!err.to_string().contains("secret-token"));
}
// Absolute on Linux, macOS, and Windows. temp_dir needs no filesystem to exist for a
// validation-only test, and Path::is_absolute stays true across platforms.
fn nats_queue_dir() -> String {
+36 -2
View File
@@ -84,7 +84,23 @@ fn redact_target_field_value(field_name: &str, value: &str) -> String {
return crate::target::mysql::redact_mysql_dsn(value);
}
if is_sensitive_target_field(field_name) {
return "***redacted***".to_string();
return crate::target::REDACTED_SECRET.to_string();
}
if field_name.eq_ignore_ascii_case(rustfs_config::WEBHOOK_ENDPOINT)
|| field_name.eq_ignore_ascii_case(rustfs_config::AMQP_URL)
{
return url::Url::parse(value)
.ok()
.and_then(|endpoint| {
let host = match endpoint.host()? {
url::Host::Domain(host) => host.to_string(),
url::Host::Ipv4(host) => host.to_string(),
url::Host::Ipv6(host) => format!("[{host}]"),
};
let port = endpoint.port().map(|port| format!(":{port}")).unwrap_or_default();
Some(format!("{}://{host}{port}", endpoint.scheme()))
})
.unwrap_or_else(|| crate::target::REDACTED_SECRET.to_string());
}
value.to_string()
}
@@ -632,6 +648,24 @@ mod tests {
assert_eq!(redact_target_field_value("queue_limit", "1000"), "1000");
}
#[test]
fn redact_target_field_value_strips_endpoint_path_and_query() {
assert_eq!(
redact_target_field_value("endpoint", "https://example.com/private/hook?token=secret"),
"https://example.com"
);
assert_eq!(redact_target_field_value("endpoint", "not a URL with secret"), "***redacted***");
}
#[test]
fn redact_target_field_value_strips_url_credentials() {
assert_eq!(
redact_target_field_value("url", "amqps://user:secret@broker.example/vhost"),
"amqps://broker.example"
);
assert_eq!(redact_target_field_value("url", "not a URL with secret"), "***redacted***");
}
#[test]
fn redact_dsn_string_partial_redaction() {
let dsn = "rustfs:secret123@tcp(mysql.example.com:3306)/rustfs_events";
@@ -671,7 +705,7 @@ mod tests {
assert_eq!(
redacted,
vec![
("endpoint".to_string(), "https://example.com/hook".to_string()),
("endpoint".to_string(), "https://example.com".to_string()),
("password".to_string(), "***redacted***".to_string()),
("client_key".to_string(), "***redacted***".to_string()),
("auth_token".to_string(), "***redacted***".to_string()),