Skip to content

Security model

Lucas Borges edited this page Oct 29, 2026 · 1 revision

tralha stores files that AI agents produce, and agents copy content from web pages, issues, logs and other sources nobody controls. So tralha assumes any upload can be hostile, that its private port may face the internet, and that keys sometimes leak.

Malicious files and image decoders

The most serious risk for a file store that processes images is a crafted file that exploits the image decoder and runs code on the server. Past attacks of this kind went through ImageMagick, Ghostscript (reached through EPS and PDF) and librsvg.

tralha does not use any of them. Pillow is the only decoder, and it can only open PNG, JPEG, GIF and WebP. Without that restriction, Image.open tries every format it supports, including EPS, which hands the file to Ghostscript.

Even Pillow never runs inside the server. Each thumbnail, compression or model preview starts a separate worker process that:

  1. starts with an empty environment, so it sees no database URL or other settings;
  2. limits itself to 1 GB of memory, 20 seconds of CPU, 64 MB of output and 64 open files, and cannot create processes (RLIMIT_NPROC=0);
  3. loads the codecs, then enables a Landlock ruleset before opening the image. From then on it cannot run any program or open TCP connections, it can read only its input file, and it can write only into a directory created for that job.

A test in the suite runs code inside a confined worker and checks that starting a shell, forking, reading /etc/passwd, writing outside the job directory and connecting to the internet all fail.

Landlock needs Linux 5.13 or newer and works inside Docker. Without it, the worker keeps the resource limits and the server logs a warning once.

Images larger than 50 megapixels, or animations above 150 megapixels across all frames, are rejected before decoding, which stops decompression bombs: small files that expand to gigabytes. Every job also has a 30-second timeout.

Malicious content served back

Uploads can contain HTML or SVG with scripts, meant to run in the browser of whoever opens a link.

  • The content type is detected from the file's first bytes, so a file uploaded as image/png that is really HTML is treated as HTML.
  • Everything served from links and from the panel's raw view carries Content-Security-Policy: sandbox, which stops scripts, forms and plugins, plus X-Content-Type-Options: nosniff.
  • HTML and unknown types are sent as downloads. Markdown is rendered through a sanitizer that strips scripts, event handlers and javascript: URLs.
  • Pages cannot be framed by other sites.

The private port on the internet

The private port can face the internet behind HTTPS. Requests to /api, /mcp and /admin without a valid key or session are rejected before their body is read; otherwise anyone could make the server buffer large bodies, since FastAPI parses bodies before checking auth. JSON bodies are capped at 1 MB.

Keys have 256 bits of randomness, link tokens 128 and upload tokens 192, so guessing one is not feasible. Even so, an IP that sends 20 bad keys, failed logins or unknown upload tokens within five minutes is locked out for fifteen, which keeps scanners out of the logs and the database. The counters live in memory and reset on restart.

The OpenAPI docs are off unless TRALHA_API_DOCS is set, and the Server header is not sent.

Leaked keys

  • Scopes limit what a key can do; an agent key can write to two buckets and delete nothing.
  • --expires bounds how long a leaked key is useful.
  • tralha key list shows when and from which IP each key was last used.
  • Only key hashes are stored, so a copy of the database does not hand out working keys.
  • Revoking a key immediately ends its panel sessions and its pending upload URLs.

The panel

Panel sessions use HttpOnly, SameSite=Strict cookies. Form posts from other origins are refused. The CSP allows scripts only from tralha itself, and the panel has none inline. TRALHA_ADMIN_ALLOWED_IPS can make the panel invisible outside your networks.

The container

The image runs as an unprivileged user with a read-only root filesystem, no Linux capabilities, no-new-privileges and a process limit. Both ports bind to localhost by default; only a proxy or an explicit change exposes them.

Known gaps

  • Bearer keys travel in every request, so HTTPS is required whenever the private port leaves your machine. Request signing in the style of S3's SigV4 would avoid that, but MCP clients send fixed headers and cannot sign requests.
  • Link and upload tokens are part of URLs and appear in access logs.
  • The code is young and has not had an external audit. Keep Pillow and the other dependencies up to date.

To report a vulnerability, open a private security advisory on the repository instead of a public issue.

Clone this wiki locally