diff --git a/crates/e2e_test/src/anonymous_access_test.rs b/crates/e2e_test/src/anonymous_access_test.rs index 25c44fe85..0891ac0a9 100644 --- a/crates/e2e_test/src/anonymous_access_test.rs +++ b/crates/e2e_test/src/anonymous_access_test.rs @@ -177,7 +177,7 @@ async fn test_anonymous_access_allowed_when_restrict_public_buckets_disabled() /// same public-access gate as a direct grant. #[tokio::test] #[serial] -async fn test_anonymous_list_object_versions_denied_when_restrict_public_buckets_enabled() +async fn ghsa_x298_anonymous_list_object_versions_denied_when_restrict_public_buckets_enabled() -> Result<(), Box> { init_logging(); info!("Starting test: anonymous ListObjectVersions denied with RestrictPublicBuckets=true..."); diff --git a/crates/policy/tests/quantified_negation.rs b/crates/policy/tests/quantified_negation.rs index 0e54f350a..eb315fcdb 100644 --- a/crates/policy/tests/quantified_negation.rs +++ b/crates/policy/tests/quantified_negation.rs @@ -26,7 +26,7 @@ fn eval(json: &str, values: &HashMap>) -> bool { } #[test] -fn for_all_values_string_not_equals_partial_overlap() { +fn ghsa_v9cp_for_all_values_not_equals_partial_overlap() { // groups = {art, hr}; policy set = {prod, art}. "art" IS in the set, // so not every value is "not equal" -> AWS says false. let v = ctx("groups", &["art", "hr"]); @@ -38,7 +38,7 @@ fn for_all_values_string_not_equals_partial_overlap() { } #[test] -fn for_any_value_string_not_equals_partial_overlap() { +fn ghsa_v9cp_for_any_value_not_equals_partial_overlap() { // groups = {art, hr}; policy set = {prod, art}. "hr" is NOT in the set, // so at least one value is "not equal" -> AWS says true. let v = ctx("groups", &["art", "hr"]); diff --git a/crates/protocols/src/common/dummy_storage.rs b/crates/protocols/src/common/dummy_storage.rs index df2ad83e4..68821bebd 100644 --- a/crates/protocols/src/common/dummy_storage.rs +++ b/crates/protocols/src/common/dummy_storage.rs @@ -217,6 +217,15 @@ pub struct DummyBackend { inner: Mutex, } +// Drivers whose trait bounds require `Debug` (for example FtpsDriver) cannot be +// instantiated with this double otherwise. The queues themselves are not worth +// rendering, and locking to print them would risk deadlocking a failing test. +impl std::fmt::Debug for DummyBackend { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DummyBackend").finish_non_exhaustive() + } +} + impl Default for DummyBackend { fn default() -> Self { Self::new() @@ -263,6 +272,18 @@ impl DummyBackend { .push_back(Ok(PutObjectOutput::default())); } + /// Queue a successful create_bucket. Authorization tests use this to tell + /// "denied before the backend" apart from "backend refused": an unqueued + /// create_bucket returns `Unconfigured`, so only a queued success proves the + /// driver reached the backend. + pub fn queue_create_bucket_ok(&self) { + self.inner + .lock() + .expect("lock") + .create_bucket + .push_back(Ok(CreateBucketOutput::default())); + } + /// Queue a put_object error. Used by the commit_write retry tests /// to script SlowDown / AccessDenied sequences against the /// rustfs_utils::retry::is_s3code_in_message_retryable predicate. diff --git a/crates/protocols/src/ftps/driver.rs b/crates/protocols/src/ftps/driver.rs index ca42cfabd..7b1225ffc 100644 --- a/crates/protocols/src/ftps/driver.rs +++ b/crates/protocols/src/ftps/driver.rs @@ -898,6 +898,37 @@ mod tests { use super::parse_s3_path; use rustfs_utils::path; + /// GHSA-g3vq-vv42-f647: MKD creates a bucket, so it must clear the + /// `s3:CreateBucket` authorization boundary before touching the backend. + /// The queued success is what makes this a real guard — an unqueued + /// create_bucket would fail on its own and the test would pass even if the + /// authorization check were removed again. + #[tokio::test] + async fn ghsa_g3vq_mkd_denied_before_reaching_backend() { + use super::FtpsDriver; + use crate::common::dummy_storage::DummyBackend; + use crate::common::gateway::with_test_auth_override; + use crate::common::session::{Protocol, test_session}; + use unftp_core::storage::StorageBackend as _; + + let backend = DummyBackend::new(); + backend.queue_create_bucket_ok(); + + let driver = FtpsDriver::new(backend); + let user = super::super::server::FtpsUser { + username: "denied-user".to_string(), + name: None, + session_context: test_session(Protocol::Ftps), + }; + + let result = with_test_auth_override(|_, _, _| false, driver.mkd(&user, "/denied-bucket")).await; + + assert!( + result.is_err(), + "MKD must fail closed when authorization denies s3:CreateBucket, even though the backend was primed to succeed" + ); + } + proptest::proptest! { #[test] fn parse_s3_path_never_leaks_control_bytes_or_traversal_in_ok_output( diff --git a/docs/testing/security-regressions.md b/docs/testing/security-regressions.md index 107ee3d05..d853dcadd 100644 --- a/docs/testing/security-regressions.md +++ b/docs/testing/security-regressions.md @@ -23,6 +23,10 @@ forced to update its pinned test (red -> green). | [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 | | [GHSA-5354-r3w2-34m8](https://github.com/rustfs/rustfs/security/advisories/GHSA-5354-r3w2-34m8) | Service-account parent must stay within caller scope — a non-owner holding `CreateServiceAccountAdminAction` could parent a service account to the root credential and authenticate as owner | rustfs/rustfs#5141 | `ghsa_5354_non_owner_service_account_parent_confined_to_scope` and the `add_service_account_parent_within_scope` invariant it pins (`rustfs/src/admin/handlers/service_account.rs`) | unit | | [GHSA-3ppv-fx5m-m749](https://github.com/rustfs/rustfs/security/advisories/GHSA-3ppv-fx5m-m749) | Versioned object reads must be authorized against `s3:GetObjectVersion`, not `s3:GetObject` (`get_object`, CopyObject source, UploadPartCopy source) | rustfs/rustfs#5142 | `ghsa_3ppv_versioned_read_selects_get_object_version_action` and the `versioned_read_action` helper it pins (`rustfs/src/storage/access.rs`) | unit | +| [GHSA-v9cp-qfw9-9pfp](https://github.com/rustfs/rustfs/security/advisories/GHSA-v9cp-qfw9-9pfp) | `ForAllValues:`/`ForAnyValue:` negated string operators were transposed — negation was applied to the aggregate quantified result instead of the per-value predicate | pending | `ghsa_v9cp_for_all_values_not_equals_partial_overlap`, `ghsa_v9cp_for_any_value_not_equals_partial_overlap` and the absent-key/positive-quantifier cases beside them (`crates/policy/tests/quantified_negation.rs`) — the value set must partially overlap the policy set, contained or disjoint sets cannot tell the two quantifiers apart | unit (crate test) | +| [GHSA-6r96-hmgc-726c](https://github.com/rustfs/rustfs/security/advisories/GHSA-6r96-hmgc-726c) | Request headers must not populate server-derived IAM condition keys (`userid`, `groups`, `jwt:`/`ldap:` claim names) | pending | `ghsa_6r96_identity_condition_keys_ignore_spoofed_headers`, `ghsa_6r96_claim_condition_keys_ignore_spoofed_headers`, and `test_request_headers_still_reach_conditions` which guards the reserved set from growing too broad (`rustfs/src/auth.rs`) | unit | +| [GHSA-x298-9x87-fvjq](https://github.com/rustfs/rustfs/security/advisories/GHSA-x298-9x87-fvjq) | The anonymous ListObjectVersions -> `s3:ListBucket` fallback must reach the same public-access gates as a direct grant | pending | `ghsa_x298_anonymous_list_object_versions_denied_when_restrict_public_buckets_enabled` (`crates/e2e_test/src/anonymous_access_test.rs`) — asserts 200 before the public-access block is applied so the test proves the gate rather than a broken fallback | e2e | +| [GHSA-g3vq-vv42-f647](https://github.com/rustfs/rustfs/security/advisories/GHSA-g3vq-vv42-f647) | FTPS `MKD` must clear the `s3:CreateBucket` authorization boundary before reaching the backend | pending | `ghsa_g3vq_mkd_denied_before_reaching_backend` (`crates/protocols/src/ftps/driver.rs`) — primes `create_bucket` to succeed so the assertion distinguishes "denied at authorization" from "backend refused" | unit | ## Where these run (CI-execution map) @@ -31,12 +35,22 @@ an unexecuted suite is theater. The suites split across three execution paths by topology: - **Unit tests** — `ghsa_r5qv_*` (`crates/ecstore`), the GHSA-m77q STS - pinning (`crates/iam`), and `ghsa_5354_*` / `ghsa_3ppv_*` (`rustfs` lib) run - automatically in the default CI pass + pinning (`crates/iam`), `ghsa_5354_*` / `ghsa_3ppv_*` / `ghsa_6r96_*` + (`rustfs` lib), `ghsa_v9cp_*` (`crates/policy` crate test), and + `ghsa_g3vq_*` (`crates/protocols`) run automatically in the default CI pass (`cargo nextest run --profile ci --all --exclude e2e_test`) — no special wiring. This is the CI-executed regression for the RPC fail-closed (r5qv), - STS-signing (m77q), service-account parent-scope (5354), and versioned-read - authorization (3ppv) advisories. + STS-signing (m77q), service-account parent-scope (5354), versioned-read + authorization (3ppv), condition-key injection (6r96), quantified-negation + (v9cp), and FTPS MKD authorization (g3vq) advisories. + + `ghsa_g3vq_*` sits behind `rustfs-protocols`' `ftps` feature, which is off by + default for that crate alone. It still runs in the workspace pass because the + `rustfs` crate defaults to `["ftps", "webdav"]` and cargo unifies features + across the build. Running `cargo test -p rustfs-protocols` on its own silently + skips it — use `--features ftps` in that case. The `protocol-features` CI + matrix covers `swift` and `sftp` only, for the same reason: those are not in + any default feature set, so they need explicit jobs. - **S3-API negative-auth e2e (e2e-smoke, PR-gated)** — the attacker-facing S3 auth-rejection suites run on every PR via the `e2e-smoke` nextest profile (`.config/nextest.toml`), which each spawns its own server on a random port diff --git a/rustfs/src/auth.rs b/rustfs/src/auth.rs index 2009e4305..731430790 100644 --- a/rustfs/src/auth.rs +++ b/rustfs/src/auth.rs @@ -1345,7 +1345,7 @@ mod tests { } #[test] - fn test_identity_condition_keys_ignore_spoofed_headers() { + fn ghsa_6r96_identity_condition_keys_ignore_spoofed_headers() { let cred = create_test_credentials(); let mut headers = HeaderMap::new(); // A caller naming its headers after identity condition keys must not be able @@ -1369,7 +1369,7 @@ mod tests { } #[test] - fn test_claim_condition_keys_ignore_spoofed_headers() { + fn ghsa_6r96_claim_condition_keys_ignore_spoofed_headers() { // The credential carries no groups/roles claims, so these keys are absent -- // precisely the case a spoofed header would otherwise fill in. let cred = create_test_credentials();