All vulnerabilities
CVE-2026-58273GHSA-5j9r-fx4x-rpvg

rucio has an Authorization Bypass on REST API endpoint for signing URLs

ruciopipLibraries & packages
CVSS
8.8High
ExploitationNo known exploitationNot in CISA KEV, which does not prove it is unexploitedCISA KEV
Exploit odds (EPSS)No scoreNo FIRST EPSS score in this snapshot yet
Fixed in35.9.0Also 38.6.0, 41.0.0rc1GitHub
Published28 Oct 2026Updated 28 Oct 2026GitHub

What fixes it

  • 35.9.0rucio, pipGitHub
  • 38.6.0rucio, pipGitHub
  • 41.0.0rc1rucio, pipGitHub
  • 35.9.0rucio, PyPIOSVpip install "rucio>=35.9.0"
  • 38.6.0rucio, PyPIOSVpip install "rucio>=38.6.0"
  • 41.0.0rc1rucio, PyPIOSVpip install "rucio>=41.0.0rc1"

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

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.

Text from GitHub Advisory Database

Affected versions 117

  • rucio, pip>= 1.16.1, < 35.9.0GitHub
  • rucio, pip>= 36.0.0rc1, < 38.6.0GitHub
  • rucio, pip>= 39.0.0rc1, < 41.0.0rc1GitHub
  • rucio, PyPIfrom 1.16.1, fixed in 35.9.0OSV
  • rucio, PyPI1.16.1OSV
  • rucio, PyPI1.16.2OSV
  • rucio, PyPI1.16.3OSV
  • rucio, PyPI1.17.0OSV
  • rucio, PyPI1.17.1OSV
  • rucio, PyPI1.17.2OSV
  • rucio, PyPI1.17.2.post1OSV
  • rucio, PyPI1.17.5OSV
  • rucio, PyPI1.17.6OSV
  • rucio, PyPI1.17.6.post1OSV
  • rucio, PyPI1.17.6.post2OSV
  • rucio, PyPI1.17.7OSV
  • rucio, PyPI1.17.8OSV
  • rucio, PyPI1.17.8.post1OSV
  • rucio, PyPI1.17.8.post2OSV
  • rucio, PyPI1.18.0OSV
  • rucio, PyPI1.18.1OSV
  • rucio, PyPI1.18.3OSV
  • rucio, PyPI1.18.4OSV
  • rucio, PyPI1.18.5OSV
  • rucio, PyPI1.18.5.post1OSV
  • rucio, PyPI1.18.6OSV
  • rucio, PyPI1.18.6.post1OSV
  • rucio, PyPI1.18.7OSV
  • rucio, PyPI1.18.8OSV
  • rucio, PyPI1.18.8.post1OSV
  • rucio, PyPI1.18.9OSV
  • rucio, PyPI1.19.0.post2OSV
  • rucio, PyPI1.19.1OSV
  • rucio, PyPI1.19.3OSV
  • rucio, PyPI1.19.4OSV
  • rucio, PyPI1.19.4.post1OSV
  • rucio, PyPI1.19.4.post2OSV
  • rucio, PyPI1.19.5OSV
  • rucio, PyPI1.19.6OSV
  • rucio, PyPI1.19.7OSV
  • rucio, PyPI1.19.7.post1OSV
  • rucio, PyPI1.19.8OSV
  • rucio, PyPI1.20.0OSV
  • rucio, PyPI1.20.0rc1OSV
  • rucio, PyPI1.20.1OSV
  • rucio, PyPI1.20.1.post1OSV
  • rucio, PyPI1.20.10OSV
  • rucio, PyPI1.20.11OSV
  • rucio, PyPI1.20.12OSV
  • rucio, PyPI1.20.13OSV
  • rucio, PyPI1.20.14OSV
  • rucio, PyPI1.20.15OSV
  • rucio, PyPI1.20.16OSV
  • rucio, PyPI1.20.2OSV
  • rucio, PyPIfrom 36.0.0rc1, fixed in 38.6.0OSV
  • rucio, PyPI36.0.0OSV
  • rucio, PyPI36.0.0.post1OSV
  • rucio, PyPI36.0.0.post2OSV
  • rucio, PyPI36.0.0rc1OSV
  • rucio, PyPI36.0.0rc2OSV
  • rucio, PyPI36.0.0rc3OSV
  • rucio, PyPI36.0.0rc4OSV
  • rucio, PyPI36.0.0rc5OSV
  • rucio, PyPI36.1.0OSV
  • rucio, PyPI36.2.0OSV
  • rucio, PyPI36.3.0OSV
  • rucio, PyPI36.4.0OSV
  • rucio, PyPI36.5.0OSV
  • rucio, PyPI37.0.0OSV
  • rucio, PyPI37.0.0rc1OSV
  • rucio, PyPI37.0.0rc2OSV
  • rucio, PyPI37.0.0rc3OSV
  • rucio, PyPI37.0.0rc4OSV
  • rucio, PyPI37.1.0OSV
  • rucio, PyPI37.1.0.post1OSV
  • rucio, PyPI37.2.0OSV
  • rucio, PyPI37.3.0OSV
  • rucio, PyPI37.4.0OSV
  • rucio, PyPI37.5.0OSV
  • rucio, PyPI37.6.0OSV
  • rucio, PyPI37.7.0OSV
  • rucio, PyPI37.7.1OSV
  • rucio, PyPI38.0.0OSV
  • rucio, PyPI38.0.0rc1OSV
  • rucio, PyPI38.0.0rc2OSV
  • rucio, PyPI38.0.0rc3OSV
  • rucio, PyPI38.1.0OSV
  • rucio, PyPI38.2.0OSV
  • rucio, PyPI38.3.0OSV
  • rucio, PyPI38.4.0OSV
  • rucio, PyPI38.5.0OSV
  • rucio, PyPI38.5.1OSV
  • rucio, PyPI38.5.2OSV
  • rucio, PyPI38.5.3OSV
  • rucio, PyPI38.5.4OSV
  • rucio, PyPI38.5.5OSV
  • rucio, PyPIfrom 39.0.0rc1, fixed in 41.0.0rc1OSV
  • rucio, PyPI39.0.0OSV
  • rucio, PyPI39.0.0rc1OSV
  • rucio, PyPI39.0.0rc2OSV
  • rucio, PyPI39.1.0OSV
  • rucio, PyPI39.2.0OSV
  • rucio, PyPI39.3.0OSV
  • rucio, PyPI39.3.1OSV
  • rucio, PyPI39.4.0OSV
  • rucio, PyPI39.4.1OSV
  • rucio, PyPI39.4.2OSV
  • rucio, PyPI40.0.0OSV
  • rucio, PyPI40.0.0rc1OSV
  • rucio, PyPI40.1.0OSV
  • rucio, PyPI40.1.1OSV
  • rucio, PyPI40.1.2OSV
  • rucio, PyPI40.2.0OSV
  • rucio, PyPI40.3.0OSV
  • rucio, PyPI40.4.0OSV
  • rucio, PyPI40.4.1OSV
  • rucio, PyPI40.4.2OSV

References 8