openleaf

Security and privacy

OpenLeaf runs a local HTTP server that reads and writes part of your filesystem. That is what makes it useful, and it is also the thing worth being careful about. This page states exactly what the boundaries are.

The threat model

What OpenLeaf protects against:

What it does not protect against, by design:

Network exposure

The server binds to 127.0.0.1. Nothing on your network can reach it, even knowing the port. This is the default and there is no setting in the interface to change it.

HOST exists as an environment variable because the server has to bind somewhere. Setting it to 0.0.0.0 publishes an unauthenticated filesystem-reading API to your whole network. There is no login, no token and no TLS, because a localhost-only server does not need them. If you want remote access, tunnel it:

ssh -L 4173:127.0.0.1:4173 you@your-machine

Cross-origin requests are refused. Any request carrying an Origin header that is not 127.0.0.1 or localhost on the same port gets a 403 before it reaches a handler.

This is not decoration. Without it, any web page you had open could issue fetch('http://127.0.0.1:4173/api/project/X/file?path=...') and read your documents, because the browser would happily send the request to localhost. The check is asserted in the test suite.

Path containment

Every path from the client goes through two gates.

Lexical resolution. The path is normalised and resolved against the project root, and the result must still be inside that root:

safeJoin('/projects/Paper', '../../../../etc/passwd')
// throws: Path escapes root

Resolving first is what makes this sound. ../ sequences and absolute paths both collapse into a resolved path that either is inside the root or is not, with no string-matching edge cases to get wrong.

Symlink resolution. For paths that already exist, the real path is checked after following links, so a symlink inside the project pointing at ~/.ssh/id_rsa is refused rather than followed.

Project names get the same treatment: a name containing a slash, or starting with a dot, is rejected outright.

Archive import

Importing a .zip is the highest-risk operation in the application, because the archive controls the filenames. The classic attack is zip-slip: an entry named ../../../../etc/cron.d/backdoor that writes outside the extraction directory.

Every entry path goes through the same safeJoin gate as any other path, so such an entry fails and the import is rejected. The test suite includes a malicious archive and asserts both that the import returns 400 and that no file appeared outside the root.

Also enforced on import: at most 4,000 files, at most 400 MB total, and build artifacts, .git and node_modules are skipped.

Data that leaves your machine

Your documents: never. No sync, no account, no telemetry, no analytics, no crash reporting, no update check.

Reference searches: only what you type. When you press ⌘⇧F and search, the query goes to four public scholarly databases:

   
DBLP computer science metadata
Crossref DOI registry
OpenAlex broad scholarly index
arXiv preprints

They receive your search terms and nothing else: no document content, no filenames, no identifier for you or your machine. The User-Agent names OpenLeaf and its repository, which is what these APIs ask for so they can contact operators about traffic patterns.

The same four are queried when you verify existing bibliography entries, which sends the title, authors and DOI of the entries being checked. Those are already published bibliographic facts.

Nothing else, ever. Work with the network off and everything except reference search behaves normally.

Shell escape

TeX can be configured to execute shell commands during compilation. This is a real risk when compiling a .tex file from an untrusted source, and it is a property of your TeX installation rather than of OpenLeaf.

OpenLeaf does not enable it. It also does not disable it, because doing so would break legitimate documents that rely on it (minted for syntax highlighting, some TikZ externalisation setups).

Modern TeX distributions ship with shell escape restricted to a small allowlist by default, which is the sensible setting. To confirm yours:

kpsewhich -var-value shell_escape
# p  means restricted to the allowlist: good
# f  means off entirely
# t  means unrestricted: change this

If you compile documents you did not write, restricted or off is what you want.

OpenLeaf does set openout_any=p, which stops the engine writing files outside the build directory through \openout.

Process isolation

The engine runs as a child process with your privileges, its working directory set to the build folder, and a hard timeout. It is not sandboxed beyond that. A document that can trigger shell escape can do what your user can do, which is why the section above matters.

Writes

Files are written to a temporary path and renamed into place. rename is atomic on every filesystem OpenLeaf supports, so:

Dependencies

The server has no runtime dependencies. It is Node’s standard library only, including the ZIP reader, the BibTeX parser and the SyncTeX parser, all of which are implemented here rather than pulled in.

The frontend bundles CodeMirror 6 and pdf.js, both widely used and actively maintained, plus esbuild as a build-time dependency. That is the entire tree, which keeps the supply-chain surface small enough to audit.

pdf.js is configured with isEvalSupported: false, so it will not evaluate JavaScript found inside a PDF.

Reporting a vulnerability

Open a security advisory rather than a public issue. Include what an attacker would need to be able to do first, and what they would gain.

Most valuable to hear about: anything that escapes the projects folder, anything that gets the server to accept a cross-origin request, and anything that causes data to be transmitted that this page says is not transmitted.