Eclipse Theia is the framework a large part of the browser-based IDE world is built on, from Eclipse Che to the Arduino IDE to a range of vendor cloud and desktop tools. In browser mode it runs a backend the editor talks to, and that backend is meant to be reachable only by the page it served. A connection token enforces that: a SameSite=Strict cookie set on the first page load, which a cross-origin page cannot obtain or send. We found that the token guarded only the WebSocket upgrade. Every plain HTTP route the backend bound was left open, and one of them, POST /file-upload, moves an uploaded file to an attacker-chosen absolute path with no workspace confinement. So a web page the victim merely visits, while a browser-mode Theia backend runs on their machine, can write a file anywhere that backend can write, and from there reach code execution on the host. Reported to the Eclipse Foundation and fixed in 1.74.0 (GHSA-62f6-wcvg-54h3, CVE-2026-60009, CWE-352 / CWE-306 / CWE-22, High).
IMPORTANT
Who’s exposed. You run Theia, or a product built on it, in browser (non-Electron) mode at 1.73.1 or earlier, and a user runs that backend on a machine where they also browse the web. That is the ordinary local-development case: an editor on localhost and a browser open in the same session. No credentials and no privileges are needed, because the request is unauthenticated by design of the flaw. You are not affected in Electron mode, which gates on a separate ElectronSecurityToken and does not reach the backend through this path.
The token guards the connection, not the request
The connection token lives in packages/core/src/node/hosting/browser-connection-token.ts. It does two separate jobs, and only one of them rejects anything. On a WebSocket upgrade, allowWsUpgrade reads the cookie and returns false when it is missing, so a tokenless or cross-origin socket is refused. On an ordinary HTTP request, expressMiddleware runs instead, and its whole body is: if the cookie is absent or stale, re-issue it, then call next(). It never returns an error. The middleware exists to hand the browser its cookie on the first page load, which is why it cannot reject: the first request legitimately has no cookie yet. But nothing else picked up the check afterward, so every HTTP route ran with the token bootstrapped and never enforced.
The route that turns that into a write is in packages/filesystem/src/node/upload/node-file-upload-service.ts. It binds POST /file-upload with a multer stage and a handler, and no guard in front. The handler reads the target from the request’s own uri field, converts it with FileUri.fsPath(), which does no normalization or containment, and calls fs.move(tmpFile, target, { overwrite: true }). The path is whatever the caller sent. An absolute path outside the workspace is accepted, and an existing file at that path is overwritten.
The last piece is what lets a foreign page reach the route at all. multipart/form-data is one of the CORS-safelisted request types, so a cross-origin fetch that posts a form body triggers no preflight and needs no cookies. The browser sends it, the backend has no HTTP-level check to stop it, and the file is written.
What the server actually returned
The evidence that isolates the defect is a pair of requests carrying the same missing token against the same running backend. One is refused, the other is served:
| Request (no connection token) | Result |
|---|---|
WebSocket handshake to /socket.io/, valid same-origin Origin | 403, the upgrade is refused |
POST /file-upload, cross-origin, absolute uri outside the workspace | 200, the file is written to that path |
GET /file?file://…, cross-origin | 200, a file outside any workspace is returned |
The first row matters because the Origin is valid: the socket is rejected on the missing token, not on the origin. That rules out the alternative reading that the deployment was simply unauthenticated. The token is present and working; it just only watches the WebSocket door. The second row is the same request the socket check would have stopped, arriving over HTTP, where nothing checks it. The third shows the gap is not specific to upload: the file-download endpoints bind through the same ungated HTTP middleware, so the same missing check reads host files as well as writes them.
The chain, from a visited page to a shell
Each step here is small, and none of them looks like a security decision on its own:
- The victim is running a browser-mode Theia backend, the default for local development, and opens a web page in the same browser session. The page is on any origin.
- The page posts a
multipart/form-databody to the backend’s/file-upload. Because that content type is CORS-safelisted, there is no preflight, and the request carries no credentials. The backend’s HTTP middleware bootstraps a cookie and callsnext(); nothing rejects the request. - The handler writes the uploaded bytes to the absolute path named in the request. On its own this is already an arbitrary write to anywhere the backend process can write.
- Point the write at a file that runs later. Overwriting a shell startup file such as
~/.bashrcplants a line that executes the next time the user opens an interactive shell. - That shell runs, and the injected command runs with it.
The write and the read are unconditional: they happen the moment the page loads. The code-execution step is conditional, in the honest sense that it needs the write to land on a path that something later executes, and for that execution to occur. Where the backend runs as root or inside a container, or where the write reaches a startup path, that condition is routinely met, which is why the advisory carries a Critical ceiling of roughly 9.6 (AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H) for those cases. The rated severity, and the floor that applies to any browser-mode deployment, is High, CVSS 8.8 (AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H): unconditional arbitrary read and write on the host.
Because /file-upload is bound by the framework rather than by any one application, the exposure is not Theia’s alone. Any product built on browser-mode Theia that ships these routes inherits the same ungated backend, which is the reach that makes a framework-level bug worth this much attention.
Remediation
If you maintain the code. The root cause is that the connection token was enforced on the WebSocket upgrade but not on HTTP requests, so the fix is to enforce it on the requests too. The 1.74.0 fix does exactly that: it adds an HttpConnectionValidator whose validateRequest returns 403 unless the request carries a valid token (and passes through in Electron), then applies it as route middleware on the sensitive HTTP endpoints, /file-upload and the file-download routes. Keeping the cookie bootstrap global while making enforcement opt-in per route is the right shape, because the initial page load genuinely has no cookie; the lesson is that every route that does real work has to opt in, and the audit is to list the routes the backend binds and check which are behind the gate. The same fix also confines the upload: it rejects any uri whose scheme is not file, and a containment check that canonicalizes the resolved path and refuses anything outside an allowed root is the durable guard against path control, independent of the token.
If you operate it. Upgrade Theia, or the product built on it, to a release based on 1.74.0 or later. Until you can:
- Do not run a browser-mode Theia backend on a machine where the same user browses untrusted web pages, and do not expose it to an untrusted network. The attack is a page visit, so the mitigation is to keep the backend off any browsing session that can reach it.
- If something in front of the deployment can require authentication on the HTTP routes, not only the WebSocket, that closes the reachable surface without depending on the backend’s own gate.
- Electron mode is not affected through this path, so where a desktop build is an option it avoids the exposure entirely.
Disclosure
- Software: Eclipse Theia in browser (non-Electron) mode, packages
@theia/coreand@theia/filesystemat 1.73.1 and earlier. - Class: CWE-352 (cross-site request forgery), CWE-306 (missing authentication on an HTTP route), and CWE-22 / CWE-73 (external control of a file path).
- Severity: High, CVSS 8.8, with a Critical ceiling of roughly 9.6 where the backend runs as root or in a container or the write reaches a startup path.
- Advisory: GHSA-62f6-wcvg-54h3, CVE-2026-60009, public 2026-08-11.
- Fix: 1.74.0.
All testing was against a backend we built and ran ourselves.
Gate the request, not the connection
A connection token names its own scope: it protects a connection. A WebSocket is a connection, and gating the upgrade gates every message that follows, so the check reads as complete once it is on the socket. But the same backend also answers plain HTTP, and HTTP has no connection to gate: each request stands alone and has to be checked on its own. A token whose mental model is “the socket is trusted once the handshake passes” quietly leaves every request that never makes a handshake unprotected, and those requests were the ones doing filesystem work.
So the question to ask of any per-connection guard is which requests actually pass through the connection it guards. Enumerate every route the server binds, not the transport you were thinking about when you wrote the check, and confirm each one is behind the gate rather than merely served by a process that has a gate somewhere. Here the gap between those two was one middleware that issued a cookie and let everything past, and a filesystem route on the other side of it.
This work sits in our web application security practice and is part of our ongoing vulnerability research into the open-source software that everyone else builds on.