CVE-2026-105080 — RCE via calibre .recipe
CVE record · GHSA-m4hh-rqmf-c8hw
Summary
ConvertX accepts calibre .recipe files as conversion input and passes them to ebook-convert, which compiles and executes the recipe as Python with the privileges of the conversion process — root in the official Docker Compose deployment. An authenticated attacker (or anyone who can reach an open registration instance) uploads a malicious .recipe file, triggers a conversion to e.g. EPUB, and obtains full remote code execution. The default Compose file also hardcodes JWT_SECRET in the process environment, so the same payload exfiltrates the secret used to sign session tokens.
Impact
- RCE as root inside the container — persistence, arbitrary file read/write.
- Secret exfiltration —
JWT_SECRETleaked via the process environment enables forging arbitrary session tokens (full authentication bypass). - Data access —
data/mydb.sqlite(user password hashes) readable from the shell. - Attack vector: network — file upload + conversion request. No special privileges beyond a ConvertX account required.
Root Cause
The converter config in src/converters/calibre.ts lists recipe among the accepted document types for the calibre converter and shells out to ebook-convert with no mitigation:
// src/converters/calibre.ts:29 — "recipe" accepted in from.document
// src/converters/calibre.ts:70 — execFile("ebook-convert", ...) without restrictions
Calibre treats .recipe files as executable Python classes (a documented feature for newspaper-style scraping recipes). ConvertX never sandboxed this path, and the official compose.yaml runs the container as root with a hardcoded JWT_SECRET.
Proof of Concept
A minimal malicious recipe that runs a shell command on import and still satisfies calibre's recipe interface:
# evil.recipe — convert recipe → epub to trigger
import os
os.system("id >> /app/data/pwned.txt; env >> /app/data/pwned.txt")
from calibre.web.feeds.news import BasicNewsRecipe
class Poc(BasicNewsRecipe):
title = "poc"
oldest_article = 1
max_articles_per_feed = 1
feeds = []
Confirmed 2026-09-21 against v0.18.0 with the official Docker Compose deployment: the output file contained uid=0(root), the full container /etc/passwd, and the JWT_SECRET environment variable. A live demo recording accompanied the private advisory.
Demo
Live exploit walkthrough — registration, malicious .recipe upload, conversion trigger, root shell and JWT_SECRET exfiltration. Also at xaferima.com/pocs.
Disclosure Timeline
| Date | Event |
|---|---|
| 2026-09-21 | Vulnerability discovered (static analysis + manual review) |
| 2026-09-21 | PoC confirmed in Docker — root shell + JWT_SECRET leak |
| 2026-09-21 | Advisory submitted via GitHub Private Vulnerability Reporting |
| 2026-09-25 | Maintainer response — fixed in v0.19.0 |
| 2026-09-29 | CVE request sent to maintainer; MITRE CNA-LR request CAN-2026-2038690 |
| 2026-10-03 | CVE-2026-105080 assigned — CVSS 4.0: 9.4 CRITICAL |
Fix
- Remove
recipefrom the accepted calibre document types (strict per-converter allowlist). - Run the container as a non-root
USER. - Do not expose secrets (e.g.
JWT_SECRET) to the conversion process environment. - Rotate the hardcoded default secret in
compose.yaml.
Fixed in ConvertX v0.19.0. CVE assignment coordinated with the maintainer and MITRE.