mirror of
https://github.com/rustfs/rustfs.git
synced 2026-08-27 23:47:28 +00:00
fix(security): enforce outbound connection policy (#5135)
Co-authored-by: cxymds <cxymds@gmail.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
Reference in New Issue
Block a user