fix(audit,targets): redact credential request headers from audit/notify entries (backlog#963) (#4459)

extract_params_header copied every request/response header verbatim into
the maps that feed audit entries (requestQuery/responseHeader) and
notification events (req_params). Sensitive headers such as
Authorization and X-Amz-Security-Token were serialized in plaintext and
forwarded to external sinks (webhook/kafka/file), leaking long-lived
credentials.

Redact credential-bearing headers at this single chokepoint: the header
name is kept for correlation, but its value is replaced with the shared
REDACTED_SECRET placeholder. A case-insensitive sensitive-header list
covers authorization, x-amz-security-token, x-amz-content-sha256,
cookie, and set-cookie. Non-sensitive headers keep their existing
behavior.

Refs: https://github.com/rustfs/backlog/issues/963
This commit is contained in:
Zhengchao An
2026-07-08 17:23:05 +08:00
committed by GitHub
parent afa3935ebb
commit ee6f791100
+60 -2
View File
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
use crate::target::REDACTED_SECRET;
use hashbrown::HashMap;
use hyper::HeaderMap;
use regex::Regex;
@@ -23,6 +24,25 @@ use std::sync::LazyLock;
use thiserror::Error;
use url::Url;
/// Request headers whose values carry credentials or session tokens and must
/// never be serialized verbatim into audit/notification entries, which are
/// forwarded to external sinks (webhook/kafka/file/...). Matched
/// case-insensitively; hyper lowercases header names, but we normalize
/// defensively so this stays correct for any caller.
const SENSITIVE_HEADERS: &[&str] = &[
"authorization",
"x-amz-security-token",
"x-amz-content-sha256",
"cookie",
"set-cookie",
];
/// Returns true when the header name is credential-bearing and its value must
/// be redacted before leaving the process.
fn is_sensitive_header(name: &str) -> bool {
SENSITIVE_HEADERS.iter().any(|h| name.eq_ignore_ascii_case(h))
}
static HOST_LABEL_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$").expect("operation should succeed"));
@@ -82,11 +102,19 @@ pub fn extract_req_params_header(head: &HeaderMap) -> HashMap<String, String> {
}
/// Extract parameters from hyper::HeaderMap, mainly header information.
///
/// Credential-bearing headers (see [`SENSITIVE_HEADERS`]) are redacted: the
/// header name is preserved for correlation, but its value is replaced with
/// [`REDACTED_SECRET`] so secrets never reach downstream audit/notification
/// sinks. Non-sensitive headers keep their existing behavior.
pub fn extract_params_header(head: &HeaderMap) -> HashMap<String, String> {
let mut params = HashMap::new();
for (key, value) in head.iter() {
if let Ok(val_str) = value.to_str() {
params.insert(key.as_str().to_string(), val_str.to_string());
let name = key.as_str();
if is_sensitive_header(name) {
params.insert(name.to_string(), REDACTED_SECRET.to_string());
} else if let Ok(val_str) = value.to_str() {
params.insert(name.to_string(), val_str.to_string());
}
}
params
@@ -432,6 +460,36 @@ mod tests {
use super::*;
use hyper::header::HeaderValue;
#[test]
fn extract_params_header_redacts_credential_headers() {
let mut headers = HeaderMap::new();
headers.insert("authorization", HeaderValue::from_static("AWS4-HMAC-SHA256 Credential=AKIA.../secret"));
headers.insert("x-amz-security-token", HeaderValue::from_static("FQoGZXIvYXdzE.../session-token"));
headers.insert("x-amz-content-sha256", HeaderValue::from_static("e3b0c44298fc1c149afbf4c8996fb924"));
headers.insert("cookie", HeaderValue::from_static("session=abc123"));
headers.insert("content-type", HeaderValue::from_static("application/octet-stream"));
headers.insert("user-agent", HeaderValue::from_static("aws-cli/2.0"));
let params = extract_params_header(&headers);
// Sensitive headers keep their name for correlation but never leak the value.
for name in ["authorization", "x-amz-security-token", "x-amz-content-sha256", "cookie"] {
assert_eq!(params.get(name).map(String::as_str), Some(REDACTED_SECRET), "{name} must be redacted");
}
// Non-sensitive headers are preserved verbatim.
assert_eq!(params.get("content-type").map(String::as_str), Some("application/octet-stream"));
assert_eq!(params.get("user-agent").map(String::as_str), Some("aws-cli/2.0"));
}
#[test]
fn is_sensitive_header_matches_case_insensitively() {
assert!(is_sensitive_header("Authorization"));
assert!(is_sensitive_header("X-Amz-Security-Token"));
assert!(is_sensitive_header("X-AMZ-CONTENT-SHA256"));
assert!(!is_sensitive_header("content-type"));
assert!(!is_sensitive_header("x-amz-request-id"));
}
#[test]
fn test_get_request_port() {
let mut headers = HeaderMap::new();