rucio has an Authorization Bypass on REST API endpoint for signing URLs
What fixes it
- 35.9.0rucio, pipGitHub
- 38.6.0rucio, pipGitHub
- 41.0.0rc1rucio, pipGitHub
- 35.9.0rucio, PyPIOSV
pip install "rucio>=35.9.0" - 38.6.0rucio, PyPIOSV
pip install "rucio>=38.6.0" - 41.0.0rc1rucio, PyPIOSV
pip 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
- github.com/rucio/rucio/commit/64c0e6a6a93ae88427ff3b7d77532ce08c8a69ecFixvia GitHub
- github.com/rucio/rucio/commit/8bdc38994a8766658d0314d36afc104c9242c96fFixvia GitHub
- github.com/rucio/rucio/commit/8d9ba31514e6f5dca32b8c1db4738582cb37ec3aFixvia GitHub
- github.com/rucio/rucio/releases/tag/35.9.0Fixvia GitHub
- github.com/rucio/rucio/releases/tag/38.6.0Fixvia GitHub
- github.com/rucio/rucio/releases/tag/41.0.0rc1Fixvia GitHub
- github.com/rucio/rucio/security/advisories/GHSA-5j9r-fx4x-rpvgAdvisoryvia GitHub
- github.com/advisories/GHSA-5j9r-fx4x-rpvgAdvisoryvia GitHub