Authentication mechanism absence or evasion In rustfs
Description
RustFS: ListServiceAccount authorizes against wrong admin action, enabling cross-user enumeration and root service account takeover ## Summary ListServiceAccount (GET /rustfs/admin/v3/list-service-accounts?user=<other>) authorizes cross-user requests against UpdateServiceAccountAdminAction instead of ListServiceAccountsAdminAction at rustfs/src/admin/handlers/service_account.rs:936. The handler accepts the wrong admin action and rejects the correct one: - A user granted only admin:UpdateServiceAccount enumerates every service account in the cluster, including the root user's (HTTP 200, full metadata). - A user granted only admin:ListServiceAccounts — the permission name every IAM document treats as "list service accounts" — receives HTTP 403 AccessDenied on the same request. Because service account access keys act as the identifier a UpdateServiceAccount holder needs to rotate a secret, and the UpdateServiceAccount handler at rustfs/src/admin/handlers/service_account.rs:489 performs no ownership check on the target access key, leaking those access keys lets a delegated "service account updater" role overwrite root-sa-1's secret, authenticate as the root user's service account, and create a persistent backdoor admin with admin:* + s3:*. Proven live end-to-end against rustfs/rustfs:latest (1.0.0-alpha.91, revision d4ea14c2) — the same revision is byte-identical on current origin/main. ## Vulnerability Details - Package: rustfs (binary crate rustfs) - Affected versions: From 0a2411f (the initial service_account.rs check-in on 2026-03-15) through current HEAD 90e584a. The vulnerable line has never been touched. - Fixed versions: None - Vulnerable file: rustfs/src/admin/handlers/service_account.rs - Vulnerable route: GET /rustfs/admin/v3/list-service-accounts?user=<other_user> (ListServiceAccount::call) - CWE: CWE-863 (Incorrect Authorization), chained with CWE-620 (Unverified Password Change) to reach CWE-269 (Improper Privilege Management) - CVSS (demonstrated chain to full admin): CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H = 10.0 Critical. If scored as Scope:Unchanged the vector is CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H = 8.8 High. The list bug alone (no chain) is CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N = 6.5 Medium and is what a maintainer would rate it if the Update ownership gap is out of scope for this report. ## Two Distinct Vulnerabilities This report documents two bugs that chain to full RustFS administrative takeover. Each is independently fixable and independently a security issue: Vulnerability A — Wrong action constant in ListServiceAccount (CWE-863) ListServiceAccount::call at line 936 checks UpdateServiceAccountAdminAction instead of ListServiceAccountsAdminAction. This is a copy-paste typo: the three sibling list handlers (lines 658, 799, 1095) all use the correct constant. The result is a permission inversion — the correct permission (admin:ListServiceAccounts) is rejected, and the wrong one (admin:UpdateServiceAccount) is accepted. Independently, this is a Medium-severity cross-user information disclosure. Vulnerability B — Missing ownership check in UpdateServiceAccount (CWE-620) UpdateServiceAccount::call at lines 489-614 authorizes on possession of admin:UpdateServiceAccount but never verifies the target ?accessKey= belongs to the caller or the caller's parent. Lines 522-525 contain a commented-out get_service_account call that would have loaded the target for such a check. This means any holder of admin:UpdateServiceAccount can overwrite any service account's secret in the cluster, regardless of ownership. Chain (A + B) — Full RustFS administrative takeover Vulnerability A leaks every service account's access key (including the root administrator's). Vulnerability B allows overwriting any SA's secret given its access key. Together: a user with a single permission (admin:UpdateServiceAccount) enumerates the root user's SA access key via the wrong-action list bug, overwrites its secret via the ownership-free update handler, authenticates as the root user's service account, and creates a persistent backdoor admin with full RustFS administrative control. Authorization mismatch at a glance: Exact policies attached to each test identity (retrieved from running server via GET /admin/v3/info-canned-policy): legit-list-pol -> {"Action": ["admin:ListServiceAccounts"], "Resource": ["arn:aws:s3:::"]} list-sa-probe-pol -> {"Action": ["admin:UpdateServiceAccount"], "Resource": ["arn:aws:s3:::"]} list-sa-restricted -> {"Action": ["admin:UpdateServiceAccount"], "Resource": ["arn:aws:s3:::probe-scope/*"]} (zero-priv-user has no attached policy) | Identity | Attached policy | GET /list-service-accounts?user=rustfsadmin | Expected | |---|---|---|---| | probe-user | list-sa-probe-pol (admin:UpdateServiceAccount) | 200 (full SA metadata) | 403 | | legit-list-user | legit-list-pol (admin:ListServiceAccounts) | 403 AccessDenied | 200 | | restricted-update-user | list-sa-restricted (admin:UpdateServiceAccount on probe-scope/*) | 200 | 403 | | zero-priv-user | (none) | 403 | 403 | | (unauthenticated) | n/a | 403 Signature required | 403 | ### Why the correct permission gets 403 The handler at line 936 calls is_allowed with the action AdminAction::UpdateServiceAccountAdminAction. The IAM engine performs an exact string match between the action in the is_allowed call (admin:UpdateServiceAccount) and the action in the caller's attached policy: - legit-list-user has policy action admin:ListServiceAccounts. This does not match admin:UpdateServiceAccount. is_allowed returns false. The handler returns 403. The user who holds the correct permission for listing service accounts is denied. - probe-user has policy action admin:UpdateServiceAccount. This matches admin:UpdateServiceAccount. is_allowed returns true. The handler returns 200. The user who holds a different, unrelated permission is granted access to a list endpoint. - restricted-update-user has the same action string but resource-scoped to arn:aws:s3:::probe-scope/*. Admin-action statements skip resource matching (crates/policy/src/policy/statement.rs:132: && !self.is_admin() && !self.is_sts()), so the resource restriction is ignored and is_allowed still returns true. There is no wildcard, superset, or inheritance relationship between these two action strings. They are separate enum variants (crates/policy/src/policy/action.rs:459-462) with distinct strum(serialize) values. The IAM engine is working correctly; the handler passes the wrong action to it. Raw request/response for legit-list-user (the counterintuitive 403): GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin HTTP/1.1 Authorization: AWS4-HMAC-SHA256 Credential=legit-list-user/... HTTP/1.1 403 AccessDeniedaccess denied Why this is not "working as intended": - admin:UpdateServiceAccount and admin:ListServiceAccounts are distinct enum variants with distinct string representations. The codebase treats them as orthogonal permissions. - Three sibling list handlers in the same file (lines 658, 799, 1095) all check ListServiceAccountsAdminAction. Only line 936 deviates. - CVE-2026-22042 / GHSA-vcwh-pff9-64cc is the maintainers' own precedent: ImportIam checking ExportIAMAction was rated Medium and fixed. The same class of bug applies here. - A zero-privilege user (no admin policies at all) cannot exploit either vulnerability — both handlers correctly enforce their respective is_allowed checks. The bug is that the list handler enforces the wrong action constant, not that it skips enforcement entirely. ## Root Cause — Vulnerability A (Wrong Action Constant) ListServiceAccount::call is registered for GET /rustfs/admin/v3/list-service-accounts at rustfs/src/admin/handlers/service_account.rs:137-141. The cross-user branch (entered when ?user=<x> does not match the caller) checks the wrong admin action: // rustfs/src/admin/handlers/service_account.rs:931-953 (HEAD 90e584a, identical at d4ea14c2) let target_account = if query.user.as_ref().is_some_and(|v| v != &cred.access_key) { if !iam_store .is_allowed(&Args { account: &cred.access_key, groups: &cred.groups, action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction), // WRONG bucket: "", conditions: &get_condition_values(...), is_owner: owner, object:
Mitigation
Update Impact
Minimal update. May introduce new vulnerabilities or breaking changes.
Ecosystem | Package | Affected version | Patched versions |
|---|---|---|---|
cargo | rustfs | 1.0.0-alpha.98 |
Aliases
References