CVE-2026-44729
Description
Twenty is an open source CRM. In 1.18.0 and earlier, the file serving endpoints in Twenty CRM at /files/* and /file/:fileFolder/:id serve uploaded files using fileStream.pipe(res) without setting any Content-Type, Content-Disposition, or X-Content-Type-Options response headers. This allows an authenticated attacker to upload an HTML file containing JavaScript, which will be rendered by the victim's browser in the context of the Twenty CRM domain when accessed — enabling session hijacking, account takeover, and data theft.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Twenty CRM 1.18.0 and earlier serves uploaded files without Content-Type or Content-Disposition headers, allowing authenticated attackers to perform stored XSS via HTML file upload, leading to session hijacking and account takeover.
Vulnerability
In Twenty CRM versions 1.18.0 and earlier, the file serving endpoints at /files/* and /file/:fileFolder/:id use fileStream.pipe(res) without setting Content-Type, Content-Disposition, or X-Content-Type-Options response headers [1]. The file sanitization only applies to SVG files; HTML, XML, and other file types pass through unchanged [1]. The vulnerable code is in file.controller.ts (line 54) and file-by-id.controller.ts (line 60) [1]. The Member role has the UPLOAD_FILE tool permission by default, allowing authenticated users to upload arbitrary files [1].
Exploitation
An authenticated attacker with the Member role can upload an HTML file containing malicious JavaScript via the file upload functionality [1]. When another user (including an administrator) accesses the uploaded file through the file serving endpoints, the browser renders the HTML and executes the script in the context of the Twenty CRM domain due to the missing Content-Type header (which defaults to text/html in many browsers) [1]. No additional user interaction beyond viewing the file is required.
Impact
Successful exploitation results in stored cross-site scripting (XSS) [1]. The attacker can steal session cookies, perform actions on behalf of the victim, exfiltrate sensitive data, and potentially achieve full account takeover [1]. The impact is high because the attacker can target administrators to gain elevated privileges.
Mitigation
The vendor has not yet released a patched version as of the advisory publication date [1]. A workaround is to configure a reverse proxy (e.g., Nginx) to set Content-Type: application/octet-stream and Content-Disposition: attachment for the /files/ and /file/ paths, or to restrict file upload permissions to trusted users only [1]. The advisory recommends upgrading once a fix is available.
AI Insight generated on May 26, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2Patches
10b13c39e022dFix participant matching failing due to updating all columns instead of only changed fields (#18105)
1 file changed · +4 −1
packages/twenty-server/src/modules/match-participant/match-participant.service.ts+4 −1 modified@@ -186,7 +186,10 @@ export class MatchParticipantService< await participantRepository.updateMany( partipantsToBeUpdated.map((participant) => ({ criteria: participant.id, - partialEntity: participant, + partialEntity: { + personId: participant.personId, + workspaceMemberId: participant.workspaceMemberId, + }, })), );
Vulnerability mechanics
Root cause
"Missing Content-Type, Content-Disposition, and X-Content-Type-Options response headers in file-serving endpoints allow uploaded HTML files to be rendered inline by the browser."
Attack vector
An authenticated attacker with the default Member role (which has `canAccessAllTools: true`) uploads an HTML file containing JavaScript via the `uploadWorkflowFile` mutation [ref_id=1]. The server returns a URL like `http://<instance>/file/workflow/<id>?token=<JWT>`. When a victim (e.g., an admin) visits that URL, the browser receives no `Content-Type` header and MIME-sniffs the response as `text/html`, causing the JavaScript to execute in the context of the Twenty CRM origin [ref_id=1]. The attacker can then steal JWT tokens from `localStorage`/`cookies` and escalate privileges [ref_id=1].
Affected code
The vulnerable endpoints are in `packages/twenty-server/src/engine/core-modules/file/controllers/file.controller.ts` (line 54, `getFile`) and `file-by-id.controller.ts` (line 60, `getFileById`). Both call `fileStream.pipe(res)` without setting `Content-Type`, `Content-Disposition`, or `X-Content-Type-Options` headers [ref_id=1]. The upload mutation `uploadWorkflowFile` in `file-workflow.resolver.ts` accepts any file type, and the sanitization in `sanitize-file.utils.ts` only covers SVG files [ref_id=1].
What the fix does
The supplied patch [patch_id=2565427] addresses an unrelated SQL error in `match-participant.service.ts` — it narrows the `updateMany` SET clause to only `personId` and `workspaceMemberId` instead of spreading the entire participant object. This does NOT fix the stored XSS vulnerability described in the advisory. The advisory recommends setting `Content-Type` based on the stored file's MIME type, adding `Content-Disposition: attachment` for non-image files, setting `X-Content-Type-Options: nosniff`, extending sanitization beyond SVGs, and/or serving user-uploaded files from a separate domain [ref_id=1]. No patch for the file-serving issue is included in the bundle.
Preconditions
- authAttacker must have an authenticated session with at least the default Member role (canAccessAllTools: true)
- inputVictim must visit the attacker's uploaded file URL while logged into the same Twenty CRM instance
- configSelf-hosted deployments or any instance not behind a reverse proxy that injects X-Content-Type-Options: nosniff are fully exploitable
- networkNetwork access to the Twenty CRM server's /metadata and /file/* endpoints
Reproduction
Step 1: Log in as a Member-role user and obtain the Bearer token from a POST /metadata request. Step 2: Upload an HTML payload via Burp Repeater using the `uploadWorkflowFile` mutation with a file named `poc.html` (Content-Type: image/png to bypass client-side checks). Step 3: Copy the returned `url` value and send a GET request to it — observe no `Content-Type`, `Content-Disposition`, or `X-Content-Type-Options` headers in the response, and the HTML/JavaScript is served verbatim. Step 4: Paste the URL into a browser where the victim is logged into Twenty CRM — the JavaScript executes on the CRM origin, exposing cookies and localStorage [ref_id=1].
Generated on May 26, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
1News mentions
0No linked articles in our index yet.