mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-03 18:55:39 +00:00
fix(admin): stop logging STS AssumeRole JWT claims (#5802)
This commit is contained in:
@@ -49,7 +49,7 @@ use serde::Deserialize;
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use serde_urlencoded::from_bytes;
|
use serde_urlencoded::from_bytes;
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{Duration, OffsetDateTime};
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{debug, error, warn};
|
||||||
|
|
||||||
const ASSUME_ROLE_ACTION: &str = "AssumeRole";
|
const ASSUME_ROLE_ACTION: &str = "AssumeRole";
|
||||||
const ASSUME_ROLE_WITH_WEB_IDENTITY_ACTION: &str = "AssumeRoleWithWebIdentity";
|
const ASSUME_ROLE_WITH_WEB_IDENTITY_ACTION: &str = "AssumeRoleWithWebIdentity";
|
||||||
@@ -76,6 +76,15 @@ fn clamp_assume_role_duration(duration_seconds: usize) -> usize {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Record that AssumeRole assembled its session claims without echoing any claim values.
|
||||||
|
///
|
||||||
|
/// Claims carry caller-supplied identity material (parent user, session policy, arbitrary
|
||||||
|
/// JWT fields); interpolating them into logs repeats the GHSA-r54g-49rx-98cr /
|
||||||
|
/// GHSA-8cm2-h255-v749 credential-leak class. Only derived metadata may be logged here.
|
||||||
|
fn trace_assume_role_claims(claims: &std::collections::HashMap<String, Value>) {
|
||||||
|
debug!(claim_count = claims.len(), "AssumeRole assembled session claims");
|
||||||
|
}
|
||||||
|
|
||||||
/// Build the site-replication IAM item that mirrors an AssumeRole temporary credential to peers.
|
/// Build the site-replication IAM item that mirrors an AssumeRole temporary credential to peers.
|
||||||
fn assume_role_site_replication_item(cred: &rustfs_credentials::Credentials, updated_at: OffsetDateTime) -> SRIAMItem {
|
fn assume_role_site_replication_item(cred: &rustfs_credentials::Credentials, updated_at: OffsetDateTime) -> SRIAMItem {
|
||||||
SRIAMItem {
|
SRIAMItem {
|
||||||
@@ -141,7 +150,7 @@ pub struct AssumeRoleHandle {}
|
|||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl Operation for AssumeRoleHandle {
|
impl Operation for AssumeRoleHandle {
|
||||||
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
async fn call(&self, req: S3Request<Body>, _params: Params<'_, '_>) -> S3Result<S3Response<(StatusCode, Body)>> {
|
||||||
warn!("handle AssumeRoleHandle");
|
debug!("handle AssumeRoleHandle");
|
||||||
|
|
||||||
let mut input = req.input;
|
let mut input = req.input;
|
||||||
|
|
||||||
@@ -241,7 +250,7 @@ async fn handle_assume_role(
|
|||||||
return Err(s3_error!(InvalidArgument, "global active sk not init"));
|
return Err(s3_error!(InvalidArgument, "global active sk not init"));
|
||||||
};
|
};
|
||||||
|
|
||||||
info!("AssumeRole get claims {:?}", &claims);
|
trace_assume_role_claims(&claims);
|
||||||
|
|
||||||
let mut new_cred = get_new_credentials_with_metadata(&claims, &secret)
|
let mut new_cred = get_new_credentials_with_metadata(&claims, &secret)
|
||||||
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("get new cred failed {e}")))?;
|
.map_err(|e| S3Error::with_message(S3ErrorCode::InternalError, format!("get new cred failed {e}")))?;
|
||||||
@@ -386,6 +395,64 @@ fn xml_escape(s: &str) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assume_role_claims_log_never_echoes_claim_values() {
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct CapturedLog(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
|
||||||
|
|
||||||
|
struct CapturedLogWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
|
||||||
|
|
||||||
|
impl std::io::Write for CapturedLogWriter {
|
||||||
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
|
self.0.lock().expect("captured log lock").extend_from_slice(buf);
|
||||||
|
Ok(buf.len())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flush(&mut self) -> std::io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'writer> tracing_subscriber::fmt::MakeWriter<'writer> for CapturedLog {
|
||||||
|
type Writer = CapturedLogWriter;
|
||||||
|
|
||||||
|
fn make_writer(&'writer self) -> Self::Writer {
|
||||||
|
CapturedLogWriter(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let captured = CapturedLog::default();
|
||||||
|
let subscriber = tracing_subscriber::fmt()
|
||||||
|
.with_ansi(false)
|
||||||
|
.without_time()
|
||||||
|
.with_max_level(tracing::Level::TRACE)
|
||||||
|
.with_writer(captured.clone())
|
||||||
|
.finish();
|
||||||
|
|
||||||
|
let mut claims = std::collections::HashMap::new();
|
||||||
|
claims.insert("parent".to_string(), Value::String("sensitive-parent-user".to_string()));
|
||||||
|
claims.insert("sessionPolicy".to_string(), Value::String("eyJzZWNyZXQtcG9saWN5LWJsb2Ii".to_string()));
|
||||||
|
claims.insert("sub".to_string(), Value::String("sensitive-subject-id".to_string()));
|
||||||
|
|
||||||
|
tracing::subscriber::with_default(subscriber, || {
|
||||||
|
trace_assume_role_claims(&claims);
|
||||||
|
});
|
||||||
|
|
||||||
|
let logs = String::from_utf8(captured.0.lock().expect("captured log lock").clone()).expect("captured logs must be UTF-8");
|
||||||
|
assert!(
|
||||||
|
logs.contains("claim_count"),
|
||||||
|
"expected the redacted claims log line to be emitted: {logs}"
|
||||||
|
);
|
||||||
|
for secret in [
|
||||||
|
"sensitive-parent-user",
|
||||||
|
"eyJzZWNyZXQtcG9saWN5LWJsb2Ii",
|
||||||
|
"sensitive-subject-id",
|
||||||
|
"sessionPolicy",
|
||||||
|
] {
|
||||||
|
assert!(!logs.contains(secret), "AssumeRole claims log leaked {secret}: {logs}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_xml_escape() {
|
fn test_xml_escape() {
|
||||||
assert_eq!(xml_escape("hello"), "hello");
|
assert_eq!(xml_escape("hello"), "hello");
|
||||||
|
|||||||
@@ -690,6 +690,17 @@ if [[ -n "$unmasked_revoke_fields" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# STS claims carry caller-supplied identity material (parent user, session policy, JWT
|
||||||
|
# fields). Interpolating the claims map into any log or error repeats the
|
||||||
|
# GHSA-r54g-49rx-98cr / GHSA-8cm2-h255-v749 credential-leak class. Only derived metadata
|
||||||
|
# such as claims.len() may be logged (see trace_assume_role_claims in sts.rs).
|
||||||
|
sts_claims_content_logs="$(rg -n '\{:\?\}.*claims|claims.*\{:\?\}|\{&?claims:\?\}|[?%]\s*&?claims\b' rustfs/src/admin/handlers/sts.rs || true)"
|
||||||
|
if [[ -n "$sts_claims_content_logs" ]]; then
|
||||||
|
echo "❌ logging guardrail violation: STS handlers must not interpolate JWT claims into logs or errors (GHSA-r54g-49rx-98cr / GHSA-8cm2-h255-v749 class); log derived metadata such as claims.len() instead" >&2
|
||||||
|
echo "$sts_claims_content_logs" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
heal_hotpath_files=(
|
heal_hotpath_files=(
|
||||||
"crates/ecstore/src/store/heal.rs"
|
"crates/ecstore/src/store/heal.rs"
|
||||||
"crates/ecstore/src/store/mod.rs"
|
"crates/ecstore/src/store/mod.rs"
|
||||||
|
|||||||
Reference in New Issue
Block a user