Budibase: Unrestricted Upload of File with Dangerous Type
Description
Summary
The file upload endpoint POST /api/attachments/process does not enforce active-content restrictions for authenticated users. The checks for dangerous file extensions (html, svg, js, php, etc.) are conditionally wrapped inside if (isPublicUser) or if (isPublicUser || !env.SELF_HOSTED), meaning any authenticated builder can upload executable web content — SVG files with inline ------WebKitFormBoundaryXXXXXXXXXXXXXXXX-- ` ``json
HTTP/1.1 200 OK
[{"size":207,"name":"xss.svg","url":"http://target:10000/files/signed/.../.svg?X-Amz-...","extension":"svg","key":"workspace_id/attachments/.svg"}] ``` ### Impact * App end users - Stored XSS on any screen containing the attachment URL. Session cookie theft → full account takeover. | * Builder accounts - If malicious URL is shared within the workspace (table attachment, embedded image), XSS fires in builder's session → workspace takeover. -------- Discovered By: Abdulrahman Albatel Abdullah Alrasheed
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Authenticated Budibase builders can upload active-content files (SVG, HTML, JS) via the attachment endpoint, leading to stored XSS for all app users.
Vulnerability
In Budibase server versions up to and including 3.30.6, the uploadFile function in packages/server/src/api/controllers/static/index.ts (lines 93–179) does not enforce active-content restrictions for authenticated users. The extension checks for dangerous file types (e.g., .html, .svg, .js, .php) are conditionally wrapped inside if (isPublicUser) or if (isPublicUser || !env.SELF_HOSTED), so an authenticated builder can upload files such as SVGs with inline ` tags, HTML files containing JavaScript, or .js` modules. These files are stored in the object store (MinIO/S3) with their correct MIME types and are served via signed URLs. [1][2][3]
Exploitation
An attacker needs a Budibase account with the Builder role (not necessarily admin) on a self-hosted Docker deployment. After authenticating via POST /api/attachments/process, the attacker uploads a crafted SVG, HTML, or JS file. The upload succeeds because the dangerous‑extension check is skipped for builders. The file is stored and a signed URL is generated. When any app user (end user of the published app) accesses that signed URL, the browser loads the file with its original MIME type and executes any embedded script, achieving stored cross‑site scripting (XSS). [2][3]
Impact
A successful attack results in persistent stored XSS over all application end users. The attacker can execute arbitrary JavaScript in the context of any victim’s browser session, potentially leading to session hijacking, data theft, or other malicious actions. No special privileges beyond the Builder role are required. [2][3]
Mitigation
Budibase released version 3.38.2 on 2026‑05‑19, which includes a security fix that blocks active content attachment uploads [4]. Users should upgrade to 3.38.2 or later. For self‑hosted deployments that cannot upgrade immediately, no official workaround is documented in the available references. [1][4]
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
11ef9af201571Block active content attachment uploads
2 files changed · +39 −26
packages/server/src/api/controllers/static/index.ts+24 −21 modified@@ -50,8 +50,12 @@ import { isWorkspaceFullyMigrated } from "../../../workspaceMigrations" const ACTIVE_CONTENT_EXTENSIONS = new Set([ "html", "htm", + "js", + "jse", + "mjs", "svg", "svgz", + "wasm", "xhtml", "mhtml", "shtml", @@ -60,6 +64,9 @@ const ACTIVE_CONTENT_EXTENSIONS = new Set([ const ACTIVE_CONTENT_MIME_TYPES = [ "text/html", "image/svg+xml", + "application/javascript", + "application/wasm", + "text/javascript", "application/xhtml+xml", ] @@ -165,29 +172,25 @@ export const uploadFile = async function ( ) } - if (isPublicUser) { - if (ACTIVE_CONTENT_EXTENSIONS.has(extensionLower)) { - throw new ActiveContentFileError(fileName) - } + if (ACTIVE_CONTENT_EXTENSIONS.has(extensionLower)) { + throw new ActiveContentFileError(fileName) + } - const mimeType = - typeof rawMimeType === "string" - ? rawMimeType.toLowerCase() - : undefined - if ( - mimeType && - ACTIVE_CONTENT_MIME_TYPES.some(type => mimeType.includes(type)) - ) { - throw new ActiveContentFileError(fileName) - } + const mimeType = + typeof rawMimeType === "string" ? rawMimeType.toLowerCase() : undefined + if ( + mimeType && + ACTIVE_CONTENT_MIME_TYPES.some(type => mimeType.includes(type)) + ) { + throw new ActiveContentFileError(fileName) + } - if ( - filePath && - (typeof filePath === "string" || Buffer.isBuffer(filePath)) && - (await detectActiveContent(filePath)) - ) { - throw new ActiveContentFileError(fileName) - } + if ( + filePath && + (typeof filePath === "string" || Buffer.isBuffer(filePath)) && + (await detectActiveContent(filePath)) + ) { + throw new ActiveContentFileError(fileName) } // filenames converted to UUIDs so they are unique
packages/server/src/api/routes/tests/attachment.spec.ts+15 −5 modified@@ -61,6 +61,15 @@ describe("/api/applications/:appId/sync", () => { )) as unknown as APIError expect(resp.message).toContain("No file provided") }) + + it("should reject active content for builder uploads", async () => { + let resp = (await config.api.attachment.process( + "xss.svg", + Buffer.from("<svg><script>alert(1)</script></svg>"), + { status: 400 } + )) as unknown as APIError + expect(resp.message).toContain("active content") + }) }) describe("/api/attachments/:tableId/upload", () => { @@ -89,13 +98,14 @@ describe("/api/applications/:appId/sync", () => { }) }) - it("should allow active content for authenticated users", async () => { - const resp = await config.api.attachment.upload( + it("should reject active content for authenticated users", async () => { + const resp = (await config.api.attachment.upload( tableId, "image.png", - Buffer.from("<svg><script>alert(1)</script></svg>") - ) - expect(resp.length).toBe(1) + Buffer.from("<svg><script>alert(1)</script></svg>"), + { status: 400 } + )) as unknown as APIError + expect(resp.message).toContain("active content") }) }) })
Vulnerability mechanics
Synthesis attempt was rejected by the grounding validator. Re-run pending.
References
3News mentions
0No linked articles in our index yet.