mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 16:28:15 +00:00
fix(notify): strip rustfs/minio internal metadata from event userMetadata (#4419)
fix(notify): strip rustfs/minio internal metadata from event userMetadata (backlog#964) Notification event construction only stripped keys prefixed with `x-amz-meta-internal-`, which is not a prefix RustFS actually uses. Real internal metadata lives under `x-rustfs-internal-*` / `x-minio-internal-*` (xl.meta compat, incl. `x-minio-internal-server-side-encryption-*`) and server-side-encryption details under `x-rustfs-encryption-*` / `x-minio-encryption-*`. None of these were filtered, so they leaked into `s3.object.userMetadata` sent to downstream notification targets (webhook/MQ) — an information disclosure of SSE/replication/healing internal state. Filter via the shared classifiers `rustfs_utils::http::is_internal_key` and `is_encryption_metadata_key` (case-insensitive, covering both key flavors), while retaining the legacy `x-amz-meta-internal-*` strip for backward compat. Genuine user metadata (`x-amz-meta-*`, content-type, ...) is preserved unchanged. Adds a regression test asserting both internal prefixes, the SSE internal key, both encryption prefixes (incl. mixed-case) and the legacy prefix are stripped while user keys survive. Refs: https://github.com/rustfs/backlog/issues/964
This commit is contained in:
@@ -16,9 +16,26 @@ use chrono::{DateTime, SecondsFormat, Utc};
|
||||
use hashbrown::HashMap;
|
||||
use rustfs_s3_ops::is_object_removed_event;
|
||||
use rustfs_s3_types::{EventName, event_schema_version};
|
||||
use rustfs_utils::http::{is_encryption_metadata_key, is_internal_key};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::form_urlencoded;
|
||||
|
||||
/// Legacy internal-metadata prefix retained for backward compatibility.
|
||||
const LEGACY_AMZ_META_INTERNAL_PREFIX: &str = "x-amz-meta-internal-";
|
||||
|
||||
/// Returns `true` if `key` is RustFS/MinIO internal metadata that must not be exposed in
|
||||
/// notification `userMetadata`. Covers the internal xl.meta prefixes
|
||||
/// (`x-rustfs-internal-*` / `x-minio-internal-*`), the server-side-encryption prefixes
|
||||
/// (`x-rustfs-encryption-*` / `x-minio-encryption-*`), and the legacy
|
||||
/// `x-amz-meta-internal-*` prefix. Case-insensitive.
|
||||
fn is_internal_metadata_key(key: &str) -> bool {
|
||||
is_internal_key(key)
|
||||
|| is_encryption_metadata_key(key)
|
||||
|| key.len() >= LEGACY_AMZ_META_INTERNAL_PREFIX.len()
|
||||
&& key.as_bytes()[..LEGACY_AMZ_META_INTERNAL_PREFIX.len()]
|
||||
.eq_ignore_ascii_case(LEGACY_AMZ_META_INTERNAL_PREFIX.as_bytes())
|
||||
}
|
||||
|
||||
/// Represents the identity of the user who triggered the event
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -258,12 +275,18 @@ impl Event {
|
||||
s3_metadata.object.size = Some(args.object.size);
|
||||
s3_metadata.object.e_tag = args.object.etag.clone();
|
||||
s3_metadata.object.content_type = args.object.content_type.clone();
|
||||
// Filter out internal reserved metadata
|
||||
// Filter out internal/reserved metadata so it never leaks to downstream
|
||||
// notification targets (webhook/MQ). RustFS stores internal metadata under both
|
||||
// `x-rustfs-internal-*` and `x-minio-internal-*` (see rustfs_utils metadata_compat),
|
||||
// and server-side-encryption details under `x-rustfs-encryption-*` /
|
||||
// `x-minio-encryption-*` (header_compat). All of these must be stripped; only genuine
|
||||
// user-defined metadata (e.g. `x-amz-meta-*`) is preserved.
|
||||
let mut user_metadata = HashMap::new();
|
||||
for (k, v) in args.object.user_defined.iter() {
|
||||
if !k.to_lowercase().starts_with("x-amz-meta-internal-") {
|
||||
user_metadata.insert(k.clone(), v.clone());
|
||||
if is_internal_metadata_key(k) {
|
||||
continue;
|
||||
}
|
||||
user_metadata.insert(k.clone(), v.clone());
|
||||
}
|
||||
s3_metadata.object.user_metadata = Some(user_metadata);
|
||||
}
|
||||
@@ -537,6 +560,65 @@ mod tests {
|
||||
assert_eq!(glacier.restore_event_data.lifecycle_restore_storage_class, "GLACIER");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_user_metadata_strips_internal_and_encryption_keys() {
|
||||
let mut user_defined = HashMap::new();
|
||||
// RustFS internal (xl.meta) keys
|
||||
user_defined.insert("x-rustfs-internal-inline-data".to_string(), "true".to_string());
|
||||
user_defined.insert("x-rustfs-internal-transition-tier".to_string(), "WARM".to_string());
|
||||
// MinIO internal keys (interop) + SSE internal metadata
|
||||
user_defined.insert("x-minio-internal-compression".to_string(), "s2".to_string());
|
||||
user_defined.insert("x-minio-internal-server-side-encryption-iv".to_string(), "secret-iv".to_string());
|
||||
// Encryption prefixes (both flavors), including mixed-case
|
||||
user_defined.insert("x-rustfs-encryption-key".to_string(), "wrapped-key".to_string());
|
||||
user_defined.insert("X-Minio-Encryption-Iv".to_string(), "secret".to_string());
|
||||
// Legacy internal prefix
|
||||
user_defined.insert("x-amz-meta-internal-foo".to_string(), "bar".to_string());
|
||||
// Genuine user metadata that MUST be preserved
|
||||
user_defined.insert("x-amz-meta-project".to_string(), "rustfs".to_string());
|
||||
user_defined.insert("content-type".to_string(), "text/plain".to_string());
|
||||
|
||||
let args = EventArgsBuilder::new(
|
||||
EventName::ObjectCreatedPut,
|
||||
"bucket",
|
||||
NotifyObjectInfo {
|
||||
bucket: "bucket".to_string(),
|
||||
name: "key".to_string(),
|
||||
user_defined,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.build();
|
||||
let event = Event::new(args);
|
||||
|
||||
let user_metadata = event
|
||||
.s3
|
||||
.object
|
||||
.user_metadata
|
||||
.expect("user_metadata should be present for a create event");
|
||||
|
||||
// All internal / encryption keys stripped.
|
||||
for internal in [
|
||||
"x-rustfs-internal-inline-data",
|
||||
"x-rustfs-internal-transition-tier",
|
||||
"x-minio-internal-compression",
|
||||
"x-minio-internal-server-side-encryption-iv",
|
||||
"x-rustfs-encryption-key",
|
||||
"X-Minio-Encryption-Iv",
|
||||
"x-amz-meta-internal-foo",
|
||||
] {
|
||||
assert!(
|
||||
!user_metadata.contains_key(internal),
|
||||
"internal key {internal:?} leaked into notification userMetadata"
|
||||
);
|
||||
}
|
||||
|
||||
// Genuine user metadata preserved unchanged.
|
||||
assert_eq!(user_metadata.get("x-amz-meta-project").map(String::as_str), Some("rustfs"));
|
||||
assert_eq!(user_metadata.get("content-type").map(String::as_str), Some("text/plain"));
|
||||
assert_eq!(user_metadata.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn event_time_serializes_with_millisecond_precision() {
|
||||
let mut event = Event::new_test_event("bucket", "key", EventName::ObjectCreatedPut);
|
||||
|
||||
Reference in New Issue
Block a user