mirror of
https://github.com/rustfs/rustfs.git
synced 2026-09-21 01:53:30 +00:00
5eee2aaead
* fix(auth): reject unsigned x-amz headers on header-signed SigV4 requests A SigV4 request authenticated with an Authorization header only binds the headers named in its SignedHeaders list, but RustFS acted on every x-amz-* header that arrived, so a replayed header-signed PutObject carrying an unsigned x-amz-copy-source became a CopyObject run as the signer that could copy any object the signer can read (GHSA-xm99-m3gq-83g8). The presigned form was already closed by GHSA-g8w9-qw9q-fghr. reject_unsigned_amz_headers_on_sigv4_request now guards S3Access::check and S3Router::check_access: every SigV4 signed-header list the request carries must cover every x-amz-* header, the Authorization header is parsed with the verifier's own s3s-sigv4 parser and compared case-insensitively, the algorithm token is pinned to AWS4-HMAC-SHA256 because the upstream header path accepts any token, and the exempt set mirrors the upstream s3s fix (x-amz-content-sha256, x-amz-decoded-content-length, x-amz-trailer, x-amz-checksum-algorithm) plus x-amz-cf-id. Adds ghsa_xm99 unit, router and e2e regressions, raises the security smoke floor to 28, and records the advisory in docs/testing/security-regressions.md and CHANGELOG.md. * chore(deps): switch s3s and s3s-sigv4 to crates.io 0.16.0 * fix(server): enforce the SigV4 header guard ahead of s3s dispatch s3s 0.16 verifies the claimed algorithm as the first step of its own signature flow, so a request whose Authorization header swaps the AWS4-HMAC-SHA256 token was answered with 501 NotImplemented before RustFS's access layer could rule on the unsigned x-amz-copy-source (GHSA-xm99-m3gq-83g8). Add the SigV4HeaderGuardLayer as the innermost external stack layer, running reject_unsigned_amz_headers_on_sigv4_request in front of s3s and serializing its rejections as the same AccessDenied S3 error document the access layer produces. --------- Co-authored-by: Hauser <housemecn@gmail.com>
9.9 KiB
9.9 KiB
Security Advisory Regression Tests
Use this when: fixing or reviewing a GHSA, or checking that an advisory's guard actually executes in CI.
Source of truth: the test files named below (rg -i ghsa finds them); .config/nextest.toml for lane membership; .github/workflows/ci.yml and .github/workflows/e2e-replication-nightly.yml for execution.
Every fixed RustFS GitHub Security Advisory maps to at least one named regression test. Name the test (or the helper / doc comment on the exact assertion) after the advisory so rg -i "ghsa|<id>" finds the guard, and a future fix of a still-open advisory is forced to update its pinned test (red -> green).
Advisory -> test map
| Advisory | Class | Fix | Named regression tests | Layer |
|---|---|---|---|---|
| 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 (e2e-protocols) |
| 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 | STS JWTs signed with the shared root secret (intentionally unfixed) | n/a; tests pin the by-design behaviour and must flip red -> green when m77q is fixed | test_ghsa_m77q_sts_session_token_signed_with_root_secret, test_created_sts_credentials_authorize_with_session_token_claims (crates/iam/src/sys.rs); token_signing_key doc (crates/iam/src/root_credentials.rs) |
unit |
| GHSA-5354-r3w2-34m8 | Service-account parent must stay within caller scope; a non-owner with CreateServiceAccountAdminAction could parent a service account to root |
rustfs/rustfs#5141 | ghsa_5354_non_owner_service_account_parent_confined_to_scope, ghsa_5354_scope_guard_matches_owner_or_self_scope_for_derived_credentials, and the add_service_account_parent_within_scope invariant they pin (rustfs/src/admin/handlers/service_account.rs) |
unit |
| GHSA-3ppv-fx5m-m749 | Versioned reads (get_object, CopyObject source, UploadPartCopy source) authorize against s3:GetObjectVersion, not s3:GetObject |
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 | ForAllValues:/ForAnyValue: negated string operators applied negation to the aggregate instead of the per-value predicate |
fixed, GHSA private-fork merge | 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, since contained or disjoint sets cannot tell the quantifiers apart |
crate test |
| GHSA-6r96-hmgc-726c | Request headers must not populate server-derived IAM condition keys (userid, groups, jwt:/ldap: claims) |
fixed, GHSA private-fork merge | ghsa_6r96_identity_condition_keys_ignore_spoofed_headers, ghsa_6r96_claim_condition_keys_ignore_spoofed_headers, and test_request_headers_still_reach_conditions, which keeps the reserved set from growing too broad (rustfs/src/auth.rs) |
unit |
| GHSA-x298-9x87-fvjq | Anonymous ListObjectVersions -> s3:ListBucket fallback must reach the same public-access gates as a direct grant |
fixed, GHSA private-fork merge | 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 it proves the gate, not a broken fallback |
e2e (e2e-smoke) |
| GHSA-g8w9-qw9q-fghr | A SigV4 presigned request must reject x-amz-* headers missing from X-Amz-SignedHeaders (tags, storage class, ACL, metadata, redirect, Object Lock, SSE) instead of applying them |
this fix | ghsa_g8w9_presigned_request_rejects_unsigned_x_amz_headers, ghsa_g8w9_presigned_request_accepts_signed_or_exempt_x_amz_headers, ghsa_g8w9_check_ignores_header_signed_sigv2_and_anonymous_requests (rustfs/src/auth.rs); ghsa_g8w9_check_access_rejects_unsigned_amz_header_on_presigned_custom_route for routes that bypass S3Access::check (rustfs/src/admin/router.rs); ghsa_g8w9_presigned_put_rejects_unsigned_x_amz_headers, ghsa_g8w9_presigned_get_rejects_unsigned_x_amz_headers, ghsa_g8w9_presigned_put_rejects_unsigned_copy_source, plus the signed-tagging control ghsa_g8w9_presigned_put_accepts_signed_x_amz_headers and the unsigned-Content-Type boundary control ghsa_g8w9_presigned_put_still_accepts_unsigned_non_amz_headers (crates/e2e_test/src/presigned_negative_test.rs) |
unit; e2e (e2e-smoke) |
| GHSA-xm99-m3gq-83g8 | A header-signed SigV4 request must reject x-amz-* headers missing from its SignedHeaders (an unsigned x-amz-copy-source turned a replayed PutObject into a cross-bucket CopyObject), and a non-AWS4-HMAC-SHA256 algorithm token must not route around that check |
this fix | ghsa_xm99_header_sigv4_rejects_unsigned_x_amz_headers, ghsa_xm99_header_sigv4_accepts_signed_or_exempt_x_amz_headers, ghsa_xm99_header_sigv4_rejects_unsupported_algorithm_token, ghsa_xm99_header_and_query_signatures_cannot_widen_each_other, ghsa_xm99_check_ignores_sigv2_jwt_and_anonymous_requests (rustfs/src/auth.rs); ghsa_xm99_check_access_rejects_unsigned_amz_header_on_header_signed_custom_route for routes that bypass S3Access::check (rustfs/src/admin/router.rs); ghsa_xm99_header_sigv4_rejects_unsigned_copy_source, which also asserts the destination bytes survive, plus the signed-copy control ghsa_xm99_header_sigv4_accepts_signed_copy_source (crates/e2e_test/src/negative_sigv4_test.rs) |
unit; e2e (e2e-smoke) |
| GHSA-g3vq-vv42-f647 | FTPS MKD must clear the s3:CreateBucket authorization boundary before reaching the backend |
fixed, GHSA private-fork merge | 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 (ftps feature) |
Where these run
| Layer | Command | Lane | Guard |
|---|---|---|---|
Unit and crate tests (ghsa_r5qv_*, the m77q pins, ghsa_5354_*, ghsa_3ppv_*, ghsa_6r96_*, ghsa_v9cp_*, ghsa_g3vq_*, ghsa_g8w9_*) |
cargo nextest run --profile ci --all --exclude e2e_test |
every PR, Test and Lint (required) |
none needed; the workspace pass runs every unit and crate test |
S3-API negative-auth e2e (negative_sigv4_test, presigned_negative_test, admin_auth_test) |
python3 scripts/e2e_binary.py build --features e2e-test-hooks, then python3 scripts/e2e_binary.py run --features e2e-test-hooks -- cargo nextest run --profile e2e-smoke -p e2e_test |
every PR, End-to-End Tests (report-only) |
scripts/check_security_smoke_count.sh with the floor in .config/security-smoke-floor.txt, run in the e2e-tests job; fails when a rename drops one of these modules out of the smoke filter |
Other S3 e2e guards (anonymous_access_test) |
same build and run as the row above | every PR, End-to-End Tests (report-only) |
scripts/check_test_wiring.py --check-profile e2e-smoke digest |
Protocol e2e (protocols::test_protocol_core_suite, GHSA-3p3x) |
python3 scripts/e2e_binary.py build --features ftps,webdav,sftp, then python3 scripts/e2e_binary.py run --features ftps,webdav,sftp -- cargo nextest run -j 1 --profile e2e-protocols -p e2e_test |
nightly, e2e-replication-nightly.yml job protocols-nightly; not PR-gated |
scripts/check_test_wiring.py --check-profile e2e-protocols digest |
Notes:
test_protocol_core_suiteis a single#[tokio::test](not#[serial]) that binds fixed ports and needs theftps,webdavbuild features; the nightly job serializes it with-j 1. It cannot join the random-port, default-featuree2e-smokeprofile. Targeted local run:crates/e2e_test/src/protocols/README.md.ghsa_g3vq_*sits behind theftpsfeature ofrustfs-protocols, which is off by default for that crate alone. It still runs in the workspace pass because therustfscrate defaults to["ftps", "webdav"]and cargo unifies features across the build;cargo test -p rustfs-protocolson its own skips it, so pass--features ftps. Theprotocol-featuresmatrix inci.ymlcovers onlyswiftandsftpfor the same reason.
Adding a new advisory guard
- Reproduce the advisory's bypass form as a focused negative test.
- Name the test (or the helper/assertion)
ghsa_<id>_*, or attach aGHSA-<id>doc comment with the advisory URL. - Add a row to the table above.
- Land it where it runs (table above): a unit guard needs nothing extra; an S3 e2e guard joins the
e2e-smokefilter in.config/nextest.tomlwith its digest updated, and a negative-auth module also bumps.config/security-smoke-floor.txt; a fixed-port protocol guard goes into the protocols suite (nightly lane).