Security model
Rendlio runs on your machines, against files you did not write. So the engine’s first assumption is that the input workbook is hostile: every byte of the .xlsx/.xlsm container is attacker-controlled — ZIP structures, XML, strings, styles, images, chart parts, formulas. This page is the contract that assumption produces, and it is the page to hand to a security reviewer.
Principles
Section titled “Principles”- The input file is hostile. Nothing in a workbook is trusted because it claims to be well-formed. Every structural assertion the file makes is verified or refused.
- No dynamic behaviour. The engine contains no dynamic code loading, no reflection over input-derived data, no process spawning, and no network I/O — enforced by a static analyzer in CI that bans the underlying APIs outright, not by convention.
- Fonts are never read from the input. There is no code path from workbook bytes to the font stack. The only fonts parsed are the ones bundled with Rendlio and the ones you place in your own font directories.
- Fail closed, fail honestly. A limit or a structural rejection produces a defined exit code, a compatibility report naming the cause, and never a truncated output document.
- Determinism is preserved. The guards introduce no output variance. There is no randomness anywhere in the engine — temp-file naming included.
What the engine never does
Section titled “What the engine never does”| Never | Consequence for you |
|---|---|
| Opens a network connection, for any purpose | No activation, no telemetry, no font downloads, no phone-home. Works unchanged on air-gapped hosts |
| Executes macros or VBA | A vbaProject.bin part is detected and disclosed in the report, never parsed beyond presence and never run |
| Refreshes external data | Live connections are not fetched; cached values render instead, and the report says so |
| Extracts input content to the file system | Container parts are decompressed to memory streams only, so path-traversal tricks have nothing to write to |
| Decrypts a protected workbook | Password-protected and legacy binary containers are refused up front (exit 3) rather than guessed at |
| Reads hardware ids, hostnames, or usernames | Licence verification is a pure offline signature check; there is no fingerprinting to disable |
| Puts cell contents in a report | detail and message strings are static templates plus counts and locations. A CI leak test enforces it, so you can share a report with support |
The guarded container walk
Section titled “The guarded container walk”An .xlsx file is a ZIP archive, which is a rich attack surface on its own. The walk is deliberately narrow:
- Size first. The file is checked against the input-size limit before it is opened.
- Magic-byte sniffing. A container that is not a ZIP is refused (exit 3). A file beginning with the legacy compound-file signature — an encrypted workbook or an old
.xls— is identified as such and refused with a message that says which, rather than being fed to a parser that would misread it. - One pass, driven by the central directory. The archive’s index is read once and part data is always located through it, never by scanning for local headers. Each entry’s local header is cross-checked against its central record; a mismatch is a refusal. Two parsers over one archive is itself the vulnerability, so there is only one.
- Part-name validation. Any entry name containing a backslash, a leading
/, an empty segment, a./..segment, a drive-letter prefix, control characters, or a percent-encoded form of the same is rejected. Rendlio also never extracts to disk, so traversal has no target even if a name slipped through. - Duplicate and unsupported entries are refused. Duplicate part names, multi-disk archives, encrypted entries, and compression methods other than Stored and Deflate all stop the run. ZIP64 is accepted within every cap.
- Only reachable parts are inflated. The parts a run decompresses are exactly the ones the package format fixes by name plus everything the workbook’s relationship graph reaches from there. An entry nobody points at is never decompressed and never read — the report describes the conversion, not the archive.
Decompression bombs
Section titled “Decompression bombs”Three absolute caps and one ratio guard bound what an archive can cost, and each is enforced on live counters as the entry inflates rather than on the sizes the file claims:
| Guard | Default |
|---|---|
| Input file size | 500 MB |
| Per-entry uncompressed bytes | 512 MiB |
| Total uncompressed bytes per run | 2 GB |
| Per-entry compression ratio | 1000:1, applied past a 1 MiB floor |
| ZIP entry count | 10,000 |
The absolute triple is what actually bounds a bomb; the ratio guard is the early exit that refuses the canonical constant-run bomb after about a megabyte instead of after half a gigabyte. The floor exists because ordinary small parts — relationship parts, printer settings, tiny XML — legitimately compress at high ratios and must not trip the guard. When an entry’s stream ends, the bytes produced must equal the size the archive declared for it; anything else is a refusal, which is also what catches a truncated or corrupted entry instead of silently rendering different content.
Every one of these is tunable — see the full resource-limit registry.
Hardened XML parsing
Section titled “Hardened XML parsing”Every XML part is read through one guarded factory; creating a reader any other way is a build error in the engine, so there is no unguarded path to keep in mind. It enforces:
- DTDs are prohibited and the entity resolver is null. Any
DOCTYPErejects the part (exit 3). This closes XXE and entity-expansion (“billion laughs”) categorically rather than by pattern-matching — no external entity is ever resolved and no entity is ever expanded. - A depth cap of 128 nested elements, so deep-nesting stack attacks hit a counter rather than the stack.
- A string-length cap on any single cell string, shared-string item, or attribute value.
- Streaming, never DOM. All spreadsheet parts are parsed as a stream, so peak memory tracks the document model rather than the raw XML.
Images
Section titled “Images”Image handling is the one place where a third-party codec touches attacker-controlled bytes, so it is guarded before the decode rather than after. These guards land together with raster-image support:
- Images per sheet are counted, and a sheet beyond the cap (default 1,000) stops the run.
- Dimensions come from the codec’s header only — no pixel decode — and an image over the pixel cap (default 100 MP) is not decoded at all: nothing is drawn in its rectangle, a warning is emitted, and the report records a
skippedoccurrence with its location. The conversion continues. - Every pixel buffer that is decoded is registered against the memory cap before it is allocated.
Limits, timeouts, and the no-partial-output rule
Section titled “Limits, timeouts, and the no-partial-output rule”Two flags do most of the work in an untrusted path:
rendlio convert untrusted.xlsx -o out.pdf --timeout 60 --max-memory 1024--timeout(default 300 s) is a wall-clock deadline, enforced cooperatively at checkpoints and backed by a watchdog that also covers code paths between them. If the pipeline is still stuck after a short grace period, the watchdog itself deletes the temp outputs, writes the report, and terminates the process with exit 4.--max-memoryis a soft cap sampled at checkpoints against the process working set. It bounds steady-state growth, not an instantaneous spike — so always set a hard container limit as the backstop. Note the current default: the CLI contract prints1024MB while the engine’s canonical registry applies no cap unless one is given. Set it explicitly if you rely on it.
Overrides are never clamped, rounded, or partially honoured: an unparseable value is a usage error (exit 3), because a limit silently weaker than you asked for is worse than a refusal. An unset or empty variable simply means “default”.
A limit hit never leaves a partial document. Every output is written to a temp name in the destination directory and atomically renamed onto the final path only after the whole document is written and flushed; multi-page PNG output renames only after every page succeeds. On any failure — limit, cancellation, internal error — the temp files are deleted, the run exits 4, and the report is still written naming the limit, its configured value, and the measured value. You never gate a pipeline on a truncated PDF.
Ctrl-C behaves the same way: cancellation flows through every stage, runs the same protocol, and exits 4 with an informational warning rather than tearing the process down mid-write.
Untrusted uploads — the sandbox profile
Section titled “Untrusted uploads — the sandbox profile”If you embed Rendlio anywhere anyone can upload a workbook, set the hardening profile. It is read once at process start:
RENDLIO_SANDBOX=1 rendlio convert upload.xlsx -o out.pdf \ --report report.json --timeout 60With it set, the engine additionally:
- hard-disables user font directories — including the environment variable route, not just the flag;
- hard-disables system font enumeration, leaving bundled fonts only (which also makes output deterministic across hosts);
- refuses
--recalculate=strict-v1(itself a planned mode — see the CLI reference); - refuses any future network-adjacent feature categorically, by a deny-list that sits next to the flag so a new feature has to pass it consciously;
- tightens the default timeout to 60 s.
Anything the sandbox forbids fails loudly — warning W8112, exit 3, one per refused option, naming the exact option spelling that was refused. A hardening profile that silently ignored the option you passed would be worse than none.
Pair it with an explicit memory cap, a lower input-size cap, and a hard container limit:
docker run --rm --memory 2g --cpus 2 \ -e RENDLIO_SANDBOX=1 -e RENDLIO_MAX_MEMORY=1073741824 -e RENDLIO_MAX_FILE_SIZE=10485760 \ -v "$PWD:/work" -w /work ghcr.io/rendlio/rendlio \ convert upload.xlsx -o out.pdf --report report.jsonThe sandbox flag is engine-level, not a playground special case — the public playground runs exactly this configuration, and so can you.
What the playground adds on top
Section titled “What the playground adds on top”The playground is the same sandboxed engine behind a web front end, and its promises are operational rather than architectural:
- Uploads and results are deleted within one hour; an expired job is indistinguishable from one that never existed.
- Each conversion runs in an isolated worker whose scratch space is wiped the moment the job finishes.
- Uploads are never used for training. Files join the test corpus only if you tick the donation box, which is off by default.
- Diagnose-only privacy mode analyses workbook structure without rendering anything, so no cell content is rendered or stored — and you still get the full compatibility report.
- Workbooks up to 10 MB; password-protected files are refused with an explanation, not a silent failure.
Full detail is in the privacy policy. If you would rather nothing left your machine at all, that is what the downloadable engine is for.
Supply chain
Section titled “Supply chain”- Every release artifact — archives, container image, packages, SBOMs — is cosign-signed from the public build pipeline, and the Install page carries the exact verification commands.
- A CycloneDX SBOM publishes with every release, listing every component in the build.
SHA256SUMScovers every artifact in a release.- Dependencies are pinned with lockfiles and restored in locked mode in CI; a build fails on any dependency outside the allowed licence set.
- Advisories for the native rendering dependencies are monitored, and a CVE affecting a shipped release triggers a patch release.
Reporting a vulnerability
Section titled “Reporting a vulnerability”Email security@rendlio.com. Please include the affected version (rendlio version output), the platform, and a reproducing workbook if one exists — a fixture that reproduces the issue is the single most useful thing you can send, and it becomes a regression test.
Responsible disclosure is appreciated: give us a chance to ship a fix before publishing. Probing the public site or playground for vulnerabilities is acceptable only as part of a report to that address — see the terms.
Next steps
Section titled “Next steps”- Deployment — the full resource-limit registry, containers, CI, air-gapped hosts
- CLI reference — every flag and environment variable, with defaults
- Compatibility report — what a refusal or a limit hit looks like in JSON
- Licensing guide — why offline verification is a security property too