GHSA-5J9R-FX4X-RPVG

Vulnerability from github – Published: 2026-10-28 19:50 – Updated: 2026-10-28 19:50
VLAI
Summary
rucio has an Authorization Bypass on REST API endpoint for signing URLs
Details

Summary

The /credentials/signurl endpoint decides authorization from the client-supplied X-Rucio-Account HTTP header instead of the authenticated token's account. Any client holding any valid auth token (even a low-privilege account they legitimately own) can send X-Rucio-Account: root to pass the root-only permission check and have the Rucio server mint a presigned read / write / delete URL for an arbitrary object, signed with the RSE's stored S3 / GCS / Swift credentials.

Details

SignURL.get() takes the authorization principal from a request header and never uses the token-authenticated identity:

# lib/rucio/web/rest/flaskapi/v1/credentials.py
account = request.headers.get('X-Rucio-Account', default=None)     # L165  (attacker-controlled)
...
result = get_signed_url(account, rse=rse, service=service,         # L194
                        operation=operation, url=url, lifetime=lifetime, vo=vo)

The gateway then uses that header value as the issuer for the permission check:

# lib/rucio/gateway/credential.py
auth_result = permission.has_permission(issuer=account, vo=vo,     # L57
                                        action='get_signed_url', kwargs=kwargs, ...)

and the permission function is root-only:

# lib/rucio/core/permission/generic.py
def perm_get_signed_url(issuer, kwargs, session):
    return _is_root(issuer)        # _is_root(x) -> x.external == 'root'

InternalAccount('root', vo).external == 'root' unconditionally, so the gate passes whenever the header is root. The endpoint is on AuthenticatedBlueprint, whose before_request hook validates the token and sets request.environ['issuer'] from the token — but nothing ever compares X-Rucio-Account to it:

# lib/rucio/web/rest/flaskapi/v1/common.py  (request_auth_env)
flask.request.environ['issuer'] = auth.get('account')             # L168  (the real account; unused by SignURL)

This is anomalous: every other authenticated handler derives the account from request.environ['issuer']; credentials.py is the only one that trusts the header. Additionally, core.credential.get_signed_url never checks that the supplied url's host/bucket belongs to the target RSE, so even a legitimate root caller gets an arbitrary-object signing oracle. In multi-VO the VO is also read from the X-Rucio-VO header, allowing cross-tenant targeting.

Proof of concept

Request (any valid token + spoofed account):

GET /credentials/signurl?rse=<cloudRSE>&svc=s3&op=delete&url=https://victim-bucket.s3.amazonaws.com/important/dataset.root HTTP/1.1
X-Rucio-Auth-Token: <any valid token, even a low-privilege account>
X-Rucio-Account: root
Accept: application/octet-stream

Response: 200 OK with a presigned DELETE URL the attacker replays directly against the storage backend.

Impact

A low-privilege Rucio user can read, overwrite, and delete arbitrary objects on any cloud-backed (S3/GCS/Swift) RSE using the server's stored credentials — enabling data exfiltration, tampering, and destruction, and cross-tenant access in multi-VO deployments.

Credit

Reported by seankohjs. Found via static analysis + manual verification.

Show details on source website

{
  "affected": [
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "rucio"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "1.16.1"
            },
            {
              "fixed": "35.9.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "rucio"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "36.0.0rc1"
            },
            {
              "fixed": "38.6.0"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    },
    {
      "package": {
        "ecosystem": "PyPI",
        "name": "rucio"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "39.0.0rc1"
            },
            {
              "fixed": "41.0.0rc1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-58273"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-10-28T19:50:05Z",
    "nvd_published_at": null,
    "severity": "HIGH"
  },
  "details": "## Summary\nThe `/credentials/signurl` endpoint decides authorization from the client-supplied `X-Rucio-Account` HTTP header instead of the authenticated token\u0027s account. Any client holding **any** valid auth token (even a low-privilege account they legitimately own) can send `X-Rucio-Account: root` to pass the root-only permission check and have the Rucio server mint a presigned **read / write / delete** URL for an **arbitrary** object, signed with the RSE\u0027s stored S3 / GCS / Swift credentials.\n\n## Details\n`SignURL.get()` takes the authorization principal from a request header and\nnever uses the token-authenticated identity:\n\n```python\n# lib/rucio/web/rest/flaskapi/v1/credentials.py\naccount = request.headers.get(\u0027X-Rucio-Account\u0027, default=None)     # L165  (attacker-controlled)\n...\nresult = get_signed_url(account, rse=rse, service=service,         # L194\n                        operation=operation, url=url, lifetime=lifetime, vo=vo)\n```\n\nThe gateway then uses that header value as the `issuer` for the permission check:\n\n```python\n# lib/rucio/gateway/credential.py\nauth_result = permission.has_permission(issuer=account, vo=vo,     # L57\n                                        action=\u0027get_signed_url\u0027, kwargs=kwargs, ...)\n```\n\nand the permission function is root-only:\n\n```python\n# lib/rucio/core/permission/generic.py\ndef perm_get_signed_url(issuer, kwargs, session):\n    return _is_root(issuer)        # _is_root(x) -\u003e x.external == \u0027root\u0027\n```\n\n`InternalAccount(\u0027root\u0027, vo).external == \u0027root\u0027` unconditionally, so the gate passes whenever the header is `root`. The endpoint is on `AuthenticatedBlueprint`, whose `before_request` hook validates the token and sets `request.environ[\u0027issuer\u0027]` **from the token** \u2014 but nothing ever compares `X-Rucio-Account` to it:\n\n```python\n# lib/rucio/web/rest/flaskapi/v1/common.py  (request_auth_env)\nflask.request.environ[\u0027issuer\u0027] = auth.get(\u0027account\u0027)             # L168  (the real account; unused by SignURL)\n```\n\nThis is anomalous: every other authenticated handler derives the account from `request.environ[\u0027issuer\u0027]`; `credentials.py` is the only one that trusts the header. Additionally, `core.credential.get_signed_url` never checks that the supplied `url`\u0027s host/bucket belongs to the target RSE, so even a legitimate `root` caller gets an arbitrary-object signing oracle. In multi-VO the VO is also read from the `X-Rucio-VO` header, allowing cross-tenant targeting.\n\n## Proof of concept\nRequest (any valid token + spoofed account):\n```\nGET /credentials/signurl?rse=\u003ccloudRSE\u003e\u0026svc=s3\u0026op=delete\u0026url=https://victim-bucket.s3.amazonaws.com/important/dataset.root HTTP/1.1\nX-Rucio-Auth-Token: \u003cany valid token, even a low-privilege account\u003e\nX-Rucio-Account: root\nAccept: application/octet-stream\n```\nResponse: `200 OK` with a presigned DELETE URL the attacker replays directly against the storage backend.\n\n## Impact\nA low-privilege Rucio user can read, overwrite, and delete arbitrary objects on any cloud-backed (S3/GCS/Swift) RSE using the server\u0027s stored credentials \u2014 enabling data exfiltration, tampering, and destruction, and cross-tenant access in multi-VO deployments.\n\n## Credit\nReported by seankohjs. Found via static analysis + manual verification.",
  "id": "GHSA-5j9r-fx4x-rpvg",
  "modified": "2026-10-28T19:50:05Z",
  "published": "2026-10-28T19:50:05Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/rucio/rucio/security/advisories/GHSA-5j9r-fx4x-rpvg"
    },
    {
      "type": "WEB",
      "url": "https://github.com/rucio/rucio/commit/64c0e6a6a93ae88427ff3b7d77532ce08c8a69ec"
    },
    {
      "type": "WEB",
      "url": "https://github.com/rucio/rucio/commit/8bdc38994a8766658d0314d36afc104c9242c96f"
    },
    {
      "type": "WEB",
      "url": "https://github.com/rucio/rucio/commit/8d9ba31514e6f5dca32b8c1db4738582cb37ec3a"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/rucio/rucio"
    },
    {
      "type": "WEB",
      "url": "https://github.com/rucio/rucio/releases/tag/35.9.0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/rucio/rucio/releases/tag/38.6.0"
    },
    {
      "type": "WEB",
      "url": "https://github.com/rucio/rucio/releases/tag/41.0.0rc1"
    }
  ],
  "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:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "rucio has an Authorization Bypass on REST API endpoint for signing URLs"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline

Sightings and recorded dates, grouped by day · UTC

JSON
How durations are calculated

Following the VTEM framework (Black Hat USA 2015), time to patch starts when the vendor or a coordinator was informed, total time to patch at discovery, and disclosure to patch at public disclosure; vendor response time runs from the report to its acknowledgement. Time to exploit and time unpatched start at the earliest discovery or report, falling back to public disclosure. Unpatched time ends at the first recorded patch, or the calculation time when no patch is recorded. Exploitation to patch shows the interval between those two events; a published proof of concept is a public exploit, not exploitation.

Each result shows its source dates. A negative interval means the end event came first; when date-only inputs leave the order open, the result says so. Date-only inputs are approximate. The earliest evidence wins: KEV report dates and sightings are labelled as such. Records describing another vulnerability are listed but never calculated from, and a PoC alone does not establish exploitation. Missing patch evidence does not prove that no patch exists. Filters below change the event list only.

Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    Related by attack behaviour

    Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.