Add comprehensive special character handling with validation refactoring and extensive test coverage (#1078)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: houseme <4829346+houseme@users.noreply.github.com>
Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
Copilot
2025-12-09 13:40:29 +08:00
committed by GitHub
parent 8de8172833
commit 20961d7c91
11 changed files with 2800 additions and 15 deletions
+47
View File
@@ -452,6 +452,31 @@ fn is_managed_sse(algorithm: &ServerSideEncryption) -> bool {
matches!(algorithm.as_str(), "AES256" | "aws:kms")
}
/// Validate object key for control characters and log special characters
///
/// This function:
/// 1. Rejects keys containing control characters (null bytes, newlines, carriage returns)
/// 2. Logs debug information for keys containing spaces, plus signs, or percent signs
///
/// The s3s library handles URL decoding, so keys are already decoded when they reach this function.
/// This validation ensures that invalid characters that could cause issues are rejected early.
fn validate_object_key(key: &str, operation: &str) -> S3Result<()> {
// Validate object key doesn't contain control characters
if key.contains(['\0', '\n', '\r']) {
return Err(S3Error::with_message(
S3ErrorCode::InvalidArgument,
format!("Object key contains invalid control characters: {:?}", key),
));
}
// Log debug info for keys with special characters to help diagnose encoding issues
if key.contains([' ', '+', '%']) {
debug!("{} object with special characters in key: {:?}", operation, key);
}
Ok(())
}
impl FS {
pub fn new() -> Self {
// let store: ECStore = ECStore::new(address, endpoint_pools).await?;
@@ -779,6 +804,10 @@ impl S3 for FS {
} => (bucket.to_string(), key.to_string(), version_id.map(|v| v.to_string())),
};
// Validate both source and destination keys
validate_object_key(&src_key, "COPY (source)")?;
validate_object_key(&key, "COPY (dest)")?;
// warn!("copy_object {}/{}, to {}/{}", &src_bucket, &src_key, &bucket, &key);
let mut src_opts = copy_src_opts(&src_bucket, &src_key, &req.headers).map_err(ApiError::from)?;
@@ -1230,6 +1259,9 @@ impl S3 for FS {
bucket, key, version_id, ..
} = req.input.clone();
// Validate object key
validate_object_key(&key, "DELETE")?;
let replica = req
.headers
.get(AMZ_BUCKET_REPLICATION_STATUS)
@@ -1692,6 +1724,9 @@ impl S3 for FS {
..
} = req.input.clone();
// Validate object key
validate_object_key(&key, "GET")?;
// Try to get from cache for small, frequently accessed objects
let manager = get_concurrency_manager();
// Generate cache key with version support: "{bucket}/{key}" or "{bucket}/{key}?versionId={vid}"
@@ -2314,6 +2349,9 @@ impl S3 for FS {
..
} = req.input.clone();
// Validate object key
validate_object_key(&key, "HEAD")?;
let part_number = part_number.map(|v| v as usize);
if let Some(part_num) = part_number {
@@ -2575,6 +2613,12 @@ impl S3 for FS {
} = req.input;
let prefix = prefix.unwrap_or_default();
// Log debug info for prefixes with special characters to help diagnose encoding issues
if prefix.contains([' ', '+', '%', '\n', '\r', '\0']) {
debug!("LIST objects with special characters in prefix: {:?}", prefix);
}
let max_keys = max_keys.unwrap_or(1000);
if max_keys < 0 {
return Err(S3Error::with_message(S3ErrorCode::InvalidArgument, "Invalid max keys".to_string()));
@@ -2798,6 +2842,9 @@ impl S3 for FS {
..
} = input;
// Validate object key
validate_object_key(&key, "PUT")?;
if if_match.is_some() || if_none_match.is_some() {
let Some(store) = new_object_layer_fn() else {
return Err(S3Error::with_message(S3ErrorCode::InternalError, "Not init".to_string()));