diff --git a/crates/e2e_test/src/security_boundary_test.rs b/crates/e2e_test/src/security_boundary_test.rs index 34d026c12..0315422ff 100644 --- a/crates/e2e_test/src/security_boundary_test.rs +++ b/crates/e2e_test/src/security_boundary_test.rs @@ -21,11 +21,13 @@ //! - SSRF prevention (internal/private endpoints rejected for tiering) //! - Race condition handling (concurrent writes converge without corruption) -use crate::common::{RustFSTestEnvironment, awscurl_put, init_logging, require_awscurl}; +use crate::common::{RustFSTestEnvironment, init_logging, signed_s3_request}; use aws_sdk_s3::error::ProvideErrorMetadata; use aws_sdk_s3::primitives::ByteStream; use aws_sdk_s3::types::{CompletedMultipartUpload, CompletedPart, Tag, Tagging}; use std::error::Error; +use std::time::Duration; +use tokio::net::TcpListener; /// Oversized tagging payloads must be rejected by the per-object tag limit. /// @@ -74,8 +76,12 @@ async fn test_large_xml_body_rejection() -> Result<(), Box Result<(), Box Result<(), Box Result<(), Box Result<(), Box> { init_logging(); - require_awscurl()?; let mut env = RustFSTestEnvironment::new().await?; env.start_rustfs_server(vec![]).await?; let tier_url = format!("{}/rustfs/admin/v3/tier", env.url); + let sentinel = TcpListener::bind("127.0.0.1:0").await?; + let sentinel_endpoint = format!("http://{}", sentinel.local_addr()?); let internal_endpoints = [ - "http://127.0.0.1:8080", - "http://localhost:8080", - "http://169.254.169.254", // cloud instance metadata endpoint - "http://[::1]:8080", + (sentinel_endpoint.as_str(), true), + ("http://localhost:8080", false), + ("http://169.254.169.254", false), // cloud instance metadata endpoint + ("http://[::1]:8080", false), ]; - for endpoint in internal_endpoints { + for (endpoint, checks_connection_attempt) in internal_endpoints { // AddTier expects an uppercase tier name and a backend configuration. let body = serde_json::json!({ "type": "s3", @@ -280,11 +286,30 @@ async fn test_tiering_url_validation() -> Result<(), BoxInvalidArgument") && response_body.contains("tier endpoint is not allowed"), + "AddTier must reject internal endpoint {endpoint} during URL validation, got: {response_body}" ); + + if checks_connection_attempt { + match tokio::time::timeout(Duration::from_secs(1), sentinel.accept()).await { + Err(_) => {} + Ok(Ok((_, peer))) => panic!("SSRF guard connected to loopback endpoint {endpoint} from {peer}"), + Ok(Err(error)) => return Err(error.into()), + } + } } env.stop_server(); diff --git a/rustfs/src/admin/handlers/tier.rs b/rustfs/src/admin/handlers/tier.rs index 03cd43714..81b8dd7d9 100644 --- a/rustfs/src/admin/handlers/tier.rs +++ b/rustfs/src/admin/handlers/tier.rs @@ -17,8 +17,9 @@ use crate::admin::runtime_sources::object_store_from_extensions; use crate::admin::storage_api::runtime_sources::TierConfigMgr; use crate::admin::storage_api::tier::{ AdminError, DailyAllTierStats, ERR_TIER_ALREADY_EXISTS, ERR_TIER_BACKEND_IN_USE, ERR_TIER_BACKEND_NOT_EMPTY, - ERR_TIER_CONNECT_ERR, ERR_TIER_INVALID_CREDENTIALS, ERR_TIER_MISSING_CREDENTIALS, ERR_TIER_NAME_NOT_UPPERCASE, - ERR_TIER_NOT_FOUND, ERR_TIER_RESERVED_NAME, TierConfig, TierConfigUpdateError, TierCreds, TierType, + ERR_TIER_CONNECT_ERR, ERR_TIER_INVALID_CONFIG, ERR_TIER_INVALID_CREDENTIALS, ERR_TIER_MISSING_CREDENTIALS, + ERR_TIER_NAME_NOT_UPPERCASE, ERR_TIER_NOT_FOUND, ERR_TIER_RESERVED_NAME, TierConfig, TierConfigUpdateError, TierCreds, + TierType, }; use crate::{ admin::runtime_sources::{current_daily_tier_stats, current_notification_system, current_tier_config_handle}, @@ -391,6 +392,8 @@ impl Operation for AddTier { S3ErrorCode::Custom("TierConnectError".into()), "tier connectivity check failed", )) + } else if err.code == ERR_TIER_INVALID_CONFIG.code { + Err(S3Error::with_message(S3ErrorCode::InvalidArgument, err.message)) } else if err.code == ERR_TIER_INVALID_CREDENTIALS.code { Err(S3Error::with_message(S3ErrorCode::Custom(err.code.clone().into()), err.message)) } else { diff --git a/rustfs/src/admin/storage_api.rs b/rustfs/src/admin/storage_api.rs index df72db34d..fdb6b8a4b 100644 --- a/rustfs/src/admin/storage_api.rs +++ b/rustfs/src/admin/storage_api.rs @@ -817,6 +817,7 @@ pub(crate) static ERR_TIER_MISSING_CREDENTIALS: AdminErrorRef = pub(crate) static ERR_TIER_ALREADY_EXISTS: AdminErrorRef = AdminErrorRef(|| &ecstore_tier::tier_handlers::ERR_TIER_ALREADY_EXISTS); pub(crate) static ERR_TIER_CONNECT_ERR: AdminErrorRef = AdminErrorRef(|| &ecstore_tier::tier_handlers::ERR_TIER_CONNECT_ERR); +pub(crate) static ERR_TIER_INVALID_CONFIG: AdminErrorRef = AdminErrorRef(|| &ecstore_tier::tier::ERR_TIER_INVALID_CONFIG); pub(crate) static ERR_TIER_INVALID_CREDENTIALS: AdminErrorRef = AdminErrorRef(|| &ecstore_tier::tier_handlers::ERR_TIER_INVALID_CREDENTIALS); pub(crate) static ERR_TIER_NAME_NOT_UPPERCASE: AdminErrorRef = @@ -963,7 +964,8 @@ pub(crate) mod s3 { pub(crate) mod tier { pub(crate) use super::{ AdminError, DailyAllTierStats, ERR_TIER_ALREADY_EXISTS, ERR_TIER_BACKEND_IN_USE, ERR_TIER_BACKEND_NOT_EMPTY, - ERR_TIER_CONNECT_ERR, ERR_TIER_INVALID_CREDENTIALS, ERR_TIER_MISSING_CREDENTIALS, ERR_TIER_NAME_NOT_UPPERCASE, - ERR_TIER_NOT_FOUND, ERR_TIER_RESERVED_NAME, TierConfig, TierConfigUpdateError, TierCreds, TierType, + ERR_TIER_CONNECT_ERR, ERR_TIER_INVALID_CONFIG, ERR_TIER_INVALID_CREDENTIALS, ERR_TIER_MISSING_CREDENTIALS, + ERR_TIER_NAME_NOT_UPPERCASE, ERR_TIER_NOT_FOUND, ERR_TIER_RESERVED_NAME, TierConfig, TierConfigUpdateError, TierCreds, + TierType, }; }