Teable: Arbitrary File Write as root via the `hash` Parameter of the Attachment Presign API

A self-registered account writes arbitrary files as root into the container via the hash parameter of the attachment presign API.

Advisory ID: TP-2026-078
Product: Teable (no-code database and spreadsheet application)
Vulnerability type: Path traversal / arbitrary file write (CWE-22, CWE-20)
CVE: CVE-2026-104100
CVSS 3.1: 8.8 (High) · CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
Affected versions: <= 1.10.0
Vendor advisory: GHSA-h4hg-45pv-pv8j
Fixed in: release.2026-07-20T11-53-25Z.2296
Reported: 16 July 2026

Summary

Teable is a no-code database with a spreadsheet interface. The presign API for file attachments took a client-supplied hash field as the on-disk filename without validating it, and the writing path had no directory jail. A self-registered user, and open registration is the default, could inject ../ sequences and create or overwrite arbitrary files inside the container. Because the backend runs as uid 0, the write happens as root, which leads to code execution through overwritten startup files. turingpoint verified the flow live and reported it responsibly.

Root cause

The presign schema declares hash as a free z.string().optional() with no format or character check (packages/openapi/src/attachment/signature.ts:32). LocalStorage.presigned adopts that value verbatim as the filename, filename = hash ?? token, and builds the target path with join(dir, filename) (apps/nestjs-backend/src/features/attachments/plugins/local.ts:93,105). The write itself in LocalStorage.save resolves the path with resolve(distPath, rename) and copies the uploaded file there via fse.copy, without checking the result against the storage directory (local.ts:199-207). The matching guard assertPathWithinStorage exists in the same module but is wired to the read path only (local.helper.ts:6, called from apps/nestjs-backend/src/features/attachments/attachments.service.ts:82,109) and runs before no write. Because self-registration and the local storage provider are defaults and the backend process runs as uid 0, a freshly created account is enough to write as root outside the upload directory.

Proof of Concept

POST /api/attachment/signature
Cookie: <session of a self-registered account>

{"type": 1, "contentType": "text/plain", "hash": "../../../../tmp/pwn"}

-> response carries the upload URL with token

PUT /api/attachment/upload/<token>

<content>

-> file is written as /tmp/pwn owned by root:root

The server accepts the traversal name as the filename, and because the write path passes no directory jail, the content lands outside the upload directory. Existing files are overwritten in the process.

Impact

  • Writing and overwriting arbitrary files across the whole container filesystem with root privileges.
  • Code execution through overwritten startup, cron, or application files.
  • Tampering with the application configuration and thereby access to every base and table of the instance.
  • Service outage through destroyed runtime or system files.

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.