DEV Community

Cover image for One unchecked filename let a popular npm document converter write to any path on disk (CVE-2026-54732)
Santosh Kumar Puppala
Santosh Kumar Puppala

Posted on

One unchecked filename let a popular npm document converter write to any path on disk (CVE-2026-54732)

TL;DR

  • libreoffice-convert, a widely used npm wrapper around LibreOffice, built the output path from a caller-supplied fileName and never reduced it to a base name.
  • A fileName containing ../ escaped the temp directory, so a caller could write arbitrary content to any path the process could write to~/.ssh/authorized_keys, an /etc/cron.d entry, a web root. Arbitrary file write, one short hop from RCE.
  • Fixed in 1.8.2 (adds path.basename). Assigned CVE-2026-54732 (Moderate, 6.5). I reported it and am credited as Reporter.

Why you should care

libreoffice-convert is a small, popular building block: apps call it to turn a DOCX/HTML/ODT buffer into a PDF. It's exactly the kind of dependency that quietly sits behind an "upload and convert" feature. And in document pipelines, the filename is very often influenced by user input — so this turns an innocent "convert my file" into "write a file wherever you want."

The setup

The API looks harmless — you hand it a buffer and a target format:

const libre = require('libreoffice-convert');
// convert a buffer to PDF, with a caller-provided output fileName
libre.convertWithOptions(inputBuffer, '.pdf', undefined, { fileName });
Enter fullscreen mode Exit fullscreen mode

Internally, the converted document is written into a temporary directory, using that fileName.

The bug

Simplified, the vulnerable path looked like this:

const outPath = path.join(tempDir.name, fileName); // fileName is caller-controlled
fs.writeFile(outPath, documentBuffer, ...);         // written verbatim
Enter fullscreen mode Exit fullscreen mode

fileName is never reduced to a base name before it's joined. And path.join is not a sandbox — it happily resolves .. segments:

path.join('/tmp/abc123', '../../../../home/app/.ssh/authorized_keys')
// => '/home/app/.ssh/authorized_keys'
Enter fullscreen mode Exit fullscreen mode

So the write lands wherever the traversal points.
Animated story: attacker-controlled filename travels App -> libreoffice-convert -> Filesystem, escapes tmpDir into an arbitrary file write, fixed by path.basename in v1.8.2

The "aha"

The library treated fileName as a name, but path.join treats it as a path — and nobody called path.basename() in between.

Proof of concept (benign)

A marker file, not a weapon — just enough to prove the write escapes the temp dir:

libre.convertWithOptions(buf, '.pdf', undefined, {
  fileName: '../../../../tmp/pwned-by-poc.pdf'
});
// -> writes to /tmp/pwned-by-poc.pdf, outside the intended temp directory
Enter fullscreen mode Exit fullscreen mode

Repoint that fileName at ~/.ssh/authorized_keys, an /etc/cron.d/* file, or a script inside a web root, and "arbitrary file write" becomes "arbitrary code execution."

The fix

Version 1.8.2 wraps the value in path.basename(fileName), which strips any directory components so the write can't leave the temp directory. One line.

If you use this package: upgrade to >= 1.8.2. If you're pinned for now, call path.basename() on any filename you pass in — and never forward a user-supplied filename unvalidated.

Takeaways

  • Any time you build a filesystem path from a caller-supplied string, path.basename() it. path.join cleans .., it doesn't contain it.
  • "It's just a filename" is a trap. In upload/convert/document pipelines, the filename is routinely attacker-influenced.
  • Write-path bugs are quieter than read-path ones and often worse: arbitrary write is a short hop to RCE via SSH keys, cron, or a web root.

Disclosure timeline

  • 2026-06-09 — reported privately to the maintainer (and via Snyk), coordinated disclosure.
  • 2026-07-04 — fix released in 1.8.2 (path.basename); advisory published.
  • CVE-2026-54732 assigned; credited as Reporter.

Advisory: GHSA-gmxc-r82q-347r / CVE-2026-54732. Thanks to the maintainer for the fast, clean fix.


Your turn: do you have a hard rule for user-supplied filenames in your services, or is it case-by-case in review? What's caught this for you — lint, a wrapper, a code-review checklist? Curious what people use.

Santosh Kumar Puppala — AI/ML Platform Architect and security researcher (multiple CVEs; creator of Norviq and Veridor). GitHub: @Santoshkumarpuppala.

Top comments (0)