mirror of
https://github.com/rustfs/rustfs.git
synced 2026-07-26 08:18:18 +00:00
test(security): pin GHSA-m77q STS root-secret token signing (#4823)
test(security): pin GHSA-m77q STS root-secret token signing (sec-7) GHSA-m77q-r63m-pj89 (intentionally UNFIXED) is that STS session tokens are signed with the shared root secret: crates/iam/src/root_credentials.rs token_signing_key() returns the root secret_key, so anyone holding the root secret can forge STS session tokens. No test named the advisory, and the existing test_created_sts_credentials_authorize_with_session_token_claims uses token_signing_key() for both signing and verifying, so it pins "same key signs and verifies" but not the m77q-specific "signing key IS the root secret" — a future fix that decouples the STS key from the root secret would pass it silently. Add a flow-level pin, test_ghsa_m77q_sts_session_token_signed_with_root_secret, that captures the advisory's exact signature: (1) token_signing_key() == the root secret; (2) an AssumeRole-style session token issued with that key decodes with the root secret and NOT with any other secret; (3) it authorizes through the STS path. All three assert CURRENT (by-design-vulnerable) behavior, so a real m77q fix (a dedicated STS signing key) turns them red and forces a red -> green regression update. Also GHSA-name the existing characterization test and token_signing_key() with doc comments and the advisory URL, and update the m77q row in docs/testing/security-regressions.md. No production behavior changes. Refs: backlog#1151 (sec-7) Co-authored-by: houseme <housemecn@gmail.com>
This commit is contained in:
@@ -22,6 +22,15 @@ pub(crate) fn credentials_or_default() -> Credentials {
|
||||
credentials().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The signing key for STS session tokens.
|
||||
///
|
||||
/// GHSA-m77q-r63m-pj89 (intentionally UNFIXED): this returns the root secret
|
||||
/// key, so STS JWTs are signed with the shared root secret and anyone holding
|
||||
/// it can forge session tokens. The behavior is pinned by
|
||||
/// `test_ghsa_m77q_sts_session_token_signed_with_root_secret` in `sys.rs`;
|
||||
/// fixing the advisory (a dedicated STS signing key distinct from the root
|
||||
/// secret) must update that test red -> green. See
|
||||
/// docs/testing/security-regressions.md.
|
||||
pub(crate) fn token_signing_key() -> Option<String> {
|
||||
credentials().map(|cred| cred.secret_key)
|
||||
}
|
||||
|
||||
@@ -1952,6 +1952,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// GHSA-m77q-r63m-pj89 (STS JWTs signed with the shared root secret —
|
||||
/// intentionally UNFIXED). This test characterizes the STS credential
|
||||
/// lifecycle: a session token signed with the active token-signing key
|
||||
/// decodes and authorizes through the STS path. It pins that the SAME key
|
||||
/// both signs and verifies STS tokens.
|
||||
///
|
||||
/// MUST UPDATE WHEN GHSA-m77q IS FIXED: the fix introduces a dedicated STS
|
||||
/// signing key distinct from the root secret. The narrower pin that captures
|
||||
/// the advisory's exact signature (signing key == root secret) is
|
||||
/// `test_ghsa_m77q_sts_session_token_signed_with_root_secret`; keep both in
|
||||
/// lockstep and flip them red -> green together.
|
||||
/// Advisory: <https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89>
|
||||
#[tokio::test]
|
||||
async fn test_created_sts_credentials_authorize_with_session_token_claims() {
|
||||
ensure_test_global_credentials();
|
||||
@@ -2053,6 +2065,95 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// GHSA-m77q-r63m-pj89 flow-level pin (STS session tokens are signed with the
|
||||
/// shared root secret — intentionally UNFIXED). Anyone holding the root secret
|
||||
/// can forge STS session tokens because RustFS reuses the root secret as the
|
||||
/// STS token-signing key. This test PINS that vulnerable-by-design behavior end
|
||||
/// to end: (1) the token-signing key IS the root secret, (2) an AssumeRole-style
|
||||
/// session token issued with that key decodes with the ROOT secret and NOT with
|
||||
/// any other secret, and (3) it authorizes through the STS path.
|
||||
///
|
||||
/// MUST UPDATE WHEN GHSA-m77q IS FIXED: the fix introduces a dedicated STS
|
||||
/// signing key distinct from the root secret. That makes assertion (1) and the
|
||||
/// "root secret decodes the token" assertion fail (red). Whoever fixes m77q must
|
||||
/// replace this pin with a regression asserting the NEW behavior — the root
|
||||
/// secret can no longer decode STS session tokens — i.e. red -> green.
|
||||
/// Advisory: <https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89>
|
||||
#[tokio::test]
|
||||
async fn test_ghsa_m77q_sts_session_token_signed_with_root_secret() {
|
||||
ensure_test_global_credentials();
|
||||
|
||||
let root = crate::root_credentials::credentials().expect("global action (root) credentials should be initialized");
|
||||
assert!(!root.secret_key.is_empty(), "root secret must be present for the STS signing-key pin");
|
||||
|
||||
// (1) m77q signature: the STS token-signing key IS the root secret. A fix
|
||||
// that introduces a dedicated STS key makes this fail -> update red->green.
|
||||
assert_eq!(
|
||||
crate::root_credentials::token_signing_key().as_deref(),
|
||||
Some(root.secret_key.as_str()),
|
||||
"GHSA-m77q pin: STS tokens are signed with the root secret. If this fails, \
|
||||
m77q may have been fixed (dedicated STS key) — update this test red->green."
|
||||
);
|
||||
|
||||
// AssumeRole-style issuance: mint a session token with the active signing key.
|
||||
// Use the mock store's registered STS parent (group `testgroup` -> readwrite)
|
||||
// so authorization resolves through the STS path; the m77q pin is about the
|
||||
// signing key, not the parent identity.
|
||||
let parent_user = "sts-fallback-test-parent";
|
||||
let signing_key = crate::root_credentials::token_signing_key().expect("STS token-signing key should be present");
|
||||
let mut claims = HashMap::new();
|
||||
claims.insert("parent".to_string(), Value::String(parent_user.to_string()));
|
||||
claims.insert(
|
||||
"exp".to_string(),
|
||||
Value::Number(serde_json::Number::from(
|
||||
(OffsetDateTime::now_utc() + time::Duration::hours(1)).unix_timestamp(),
|
||||
)),
|
||||
);
|
||||
let mut cred = get_new_credentials_with_metadata(&claims, &signing_key).expect("STS credentials should be created");
|
||||
cred.parent_user = parent_user.to_string();
|
||||
|
||||
// (2) The session token decodes with the ROOT secret specifically — the crux
|
||||
// of m77q — and rejects any non-root secret (HMAC verification fails).
|
||||
let decoded = get_claims_from_token_with_secret(&cred.session_token, &root.secret_key)
|
||||
.expect("GHSA-m77q pin: STS session token must decode with the ROOT secret");
|
||||
assert_eq!(decoded.get("parent").and_then(Value::as_str), Some(parent_user));
|
||||
assert!(
|
||||
get_claims_from_token_with_secret(&cred.session_token, "not-the-root-secret").is_err(),
|
||||
"GHSA-m77q pin: STS session token must NOT decode with a non-root secret"
|
||||
);
|
||||
|
||||
// (3) The root-secret-signed STS credentials authorize end to end.
|
||||
let store = StsTestMockStore::new(false);
|
||||
let cache_manager = IamCache::new(store).await.unwrap();
|
||||
let iam_sys = IamSys::new(cache_manager);
|
||||
iam_sys
|
||||
.set_temp_user(&cred.access_key, &cred, None)
|
||||
.await
|
||||
.expect("STS credentials should be persisted in the temp-user cache");
|
||||
|
||||
let groups: Option<Vec<String>> = None;
|
||||
let args = Args {
|
||||
account: &cred.access_key,
|
||||
groups: &groups,
|
||||
action: Action::S3Action(S3Action::ListBucketAction),
|
||||
bucket: "mybucket",
|
||||
conditions: &HashMap::new(),
|
||||
is_owner: false,
|
||||
object: "",
|
||||
claims: &decoded,
|
||||
deny_only: false,
|
||||
};
|
||||
let prepared = iam_sys.prepare_auth(&args).await;
|
||||
assert!(
|
||||
matches!(prepared.mode, PreparedIamMode::Sts { .. }),
|
||||
"root-secret-signed STS credentials must use the STS authorization path"
|
||||
);
|
||||
assert!(
|
||||
iam_sys.eval_prepared(&prepared, &args).await,
|
||||
"root-secret-signed STS credentials should authorize through the parent group policy (m77q)"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression test: temp credentials without groups in args still receive group-attached
|
||||
/// policies via the parent user (groups fallback). Without the fallback, policy_db_get
|
||||
/// would get None for groups and the user would have no group policies, so the action
|
||||
|
||||
@@ -20,7 +20,7 @@ forced to update its pinned test (red -> green).
|
||||
| --- | --- | --- | --- | --- |
|
||||
| [GHSA-3p3x-734c-h5vx](https://github.com/rustfs/rustfs/security/advisories/GHSA-3p3x-734c-h5vx) | Constant-time secret comparison on WebDAV/FTPS password login | rustfs/rustfs#4403 | `assert_ftps_ghsa_3p3x_wrong_credentials_rejected` (`crates/e2e_test/src/protocols/ftps_core.rs`); `GHSA-3p3x` auth-failure block in `test_webdav_core_operations` (`crates/e2e_test/src/protocols/webdav_core.rs`) | e2e (protocols suite) |
|
||||
| [GHSA-r5qv-rc46-hv8q](https://github.com/rustfs/rustfs/security/advisories/GHSA-r5qv-rc46-hv8q) | Internode RPC authentication must fail closed | rustfs/rustfs#4402 | `ghsa_r5qv_resolve_shared_secret_rejects_default_fallback`, `ghsa_r5qv_verify_rpc_signature_fails_closed_on_missing_or_invalid_auth` (`crates/ecstore/src/cluster/rpc/http_auth.rs`) | unit |
|
||||
| [GHSA-m77q-r63m-pj89](https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89) | STS JWTs signed with shared root secret (intentionally unfixed) | n/a | `test_created_sts_credentials_authorize_with_session_token_claims` (`crates/iam/src/sys.rs`) — pins current behavior; sec-7 adds GHSA naming | unit |
|
||||
| [GHSA-m77q-r63m-pj89](https://github.com/rustfs/rustfs/security/advisories/GHSA-m77q-r63m-pj89) | STS JWTs signed with shared root secret (intentionally unfixed) | n/a | `test_ghsa_m77q_sts_session_token_signed_with_root_secret` (flow-level pin: signing key == root secret, root-only decode, authorizes) and `test_created_sts_credentials_authorize_with_session_token_claims` (`crates/iam/src/sys.rs`); `token_signing_key` doc (`crates/iam/src/root_credentials.rs`) — pin current by-design behavior; fixing m77q must update red -> green | unit |
|
||||
|
||||
## Where these run (CI-execution map)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user