---
id: "CVE-2026-58273"
kind: "vulnerability"
aliases: ["CVE-2026-58273", "GHSA-5j9r-fx4x-rpvg"]
title: "rucio has an Authorization Bypass on REST API endpoint for signing URLs"
severity: "high"
cvss: 8.8
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "rucio", "fixed": ["35.9.0"], "affected": ">= 1.16.1, < 35.9.0", "ecosystem": "pypi"}, {"name": "rucio", "fixed": ["38.6.0"], "affected": ">= 36.0.0rc1, < 38.6.0", "ecosystem": "pypi"}, {"name": "rucio", "fixed": ["41.0.0rc1"], "affected": ">= 39.0.0rc1, < 41.0.0rc1", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["cwe-639", "gcs", "multi-vo", "package", "rucio", "s3", "self-hosted", "swift"]
not_affected_if: ["api-restricted-to-trusted-users", "no-cloud-rse", "versions-outside-range"]
published_at: "2026-10-28T19:50:05+00:00"
first_seen_at: "2026-10-28T19:51:20+00:00"
updated_at: "2026-10-28T19:52:21+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-5j9r-fx4x-rpvg", "first_seen_at": "2026-10-28T19:51:20+00:00"}]
---

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

Rucio's /credentials/signurl endpoint takes the account from a client-supplied X-Rucio-Account header instead of the authenticated token, so setting it to root passes the root-only check. An attacker with any valid token can get presigned read, write or delete URLs for arbitrary objects on cloud-backed RSEs, signed with the server's stored S3, GCS or Swift credentials.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:rucio affected >= 1.16.1, < 35.9.0 fixed 35.9.0
- pypi:rucio affected >= 36.0.0rc1, < 38.6.0 fixed 38.6.0
- pypi:rucio affected >= 39.0.0rc1, < 41.0.0rc1 fixed 41.0.0rc1

## How to check

- Run `pip show rucio` on the Rucio server and compare the version against the fixed releases.
- Check whether any RSE uses S3, GCS or Swift storage, since the flaw only affects cloud-backed RSEs.
- Check web server or access logs for requests to `/credentials/signurl` that carry an `X-Rucio-Account` header.

## Fix

- Upgrade rucio to `35.9.0` on the 35.x line, `38.6.0` on the 38.x line, or `41.0.0rc1` on the 39 to 41 line.
- Rotate the S3, GCS and Swift credentials stored for affected RSEs if signing requests were seen from untrusted clients.
- Block or strip the `X-Rucio-Account` header at the reverse proxy until the upgrade is in place.

## Timeline

- 2026-10-28T19:51:20+00:00 ghsa https://github.com/advisories/GHSA-5j9r-fx4x-rpvg

## From the sources

## 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:

```python
# 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:

```python
# 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:

```python
# 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:

```python
# 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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
