GHSA-5J9R-FX4X-RPVG
Vulnerability from github – Published: 2026-10-28 19:50 – Updated: 2026-10-28 19:50Summary
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.
{
"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"
}
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.
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
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.
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
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.