-
Notifications
You must be signed in to change notification settings - Fork 0
Security
This page states the threat model plainly, what has been defended, and what is honestly still open. The project audits itself and files every finding as a public GitHub issue — the current register is the security label, open and closed alike. Nothing on this page is softened: a converter that reads other people's files is a parser exposed to hostile input, and pretending otherwise would be the real vulnerability.
A .docx is attacker-controlled input, and n8PDF exists to read it. Concretely, one document can carry all of the following, and every one of them is parsed by code in this repository:
- a ZIP container whose headers describe its own sizes — and a hostile file describes them wrongly
- XML parts (the document, styles, numbering, charts, diagrams, settings…)
- images in six formats — PNG, GIF, BMP, TIFF (including LZW, PackBits, Deflate and fax-encoded strips, tiles, planar layouts), EMF/EMF+ metafiles, and JPEG (sequential, progressive, arithmetic)
-
embedded fonts (
w:embedRegular) — since these were implemented, the SFNT/OpenType/AAT parsers read attacker-controlled bytes too - charts, equations and diagrams, each with counts, offsets and recursion of their own
Because the library is pure managed .NET with no native code and no unsafe, the realistic failure modes are denial of service, not memory corruption: unbounded allocation (OOM), unbounded work (hangs), stack overflow (which kills a .NET process outright — it cannot be caught), and uncaught exceptions aborting a conversion. Those are the attacks the audits look for, find, and file.
Two attacks were cheap to write and were both once open here; both are now closed and the tests build each attack rather than describing it:
-
A part that decompresses without bound. Zeros compress about a thousand to one.
PackageLimitsbounds it: 128 MB for one part, 512 MB across a package, 4096 parts. Limits are counted against what actually comes out of the decompressor, never what a header claims — so a lying header cannot smuggle anything past. Raise them viaConversionOptions.Limitsfor a document that genuinely needs more, and catchPackageTooLargeExceptionto know that is what happened. - XML entity expansion (billion laughs). Ten entities, each ten of the one below, expand a kilobyte into a gigabyte — an attack no counting of compressed bytes can catch. Parts are read through a reader that prohibits DTDs outright, which costs nothing legitimate: the Open Packaging Conventions forbid a DTD in a part.
An image declares its own size in its header, and decoders allocate from what it says before reading a byte of the picture — a 57-byte PNG can call itself fifty thousand pixels square and ask for 7.5 GB. MaximumImagePixels bounds the declared area (default 50 million — a 600dpi A4 scan with room to spare), counted in long arithmetic since 70,000² does not fit the int the pixels would have been allocated with. A picture past the limit is left out the way any unreadable picture is: the document loses the picture, not the conversion.
The audit reproduced crafted files of a few dozen bytes escaping the decoders' own error handling with runtime exceptions — index-out-of-range, overflow, divide-by-zero and the like — that flew straight out of the public API. The conversion-level net now catches exactly the types the audit reproduced, so a malformed image costs its own placement, not the conversion (#180). OutOfMemoryException is deliberately not caught — that one means the process is in trouble, and hiding it helps nobody. The net is the net, not the holes: each underlying validation gap stays filed as its own issue with its own fix and decoder-level regression test.
-
Embedded fonts are bounded by
PackageLimits.MaximumFontBytes; a face that will not parse is left out and the conversion proceeds in substitutes, exactly as if it had not been carried. Registering a face into aFontLibraryvalidates it (FontFormatException). - The SFNT table directory clamps declared table lengths to what is actually in the file — a malformed face can no longer declare a two-gigabyte table and be believed.
- When embedded fonts landed, the whole of
Fonts/was re-audited as an untrusted-input surface (#171); its findings are filed individually (see below).
Every attack-shaped finding — however small — is filed with a reproduction, a severity, and a statement of what a hostile .docx gets out of it. Two critical findings (unbounded recursion in nested-TIFF-in-JPEG strips, and an EMF+ image recursing back into the image reader — each a StackOverflowException that kills the process) were fixed the way everything here is fixed: with the attack built as a test.
The register is public and this summary will age; the live security label is the truth. As of August 2026, roughly eighty open findings, almost all denial-of-service-shaped, concentrated in:
-
Image decoders (the largest group, tracked under hardening epics #49–#56): unvalidated counts used as allocation sizes,
intoverflow slipping past bounds checks, uncaught exceptions from malformed streams, and per-decoder gaps in TIFF, BMP, GIF, PNG, JPEG, CCITT and EMF/EMF+. The conversion-level net (above) stops these aborting a conversion; the memory- and CPU-exhaustion holes remain until each is individually closed. -
Parsers and layout: unbounded recursion in nested tables, text boxes, equation markup and inline wrappers (stack overflow kills the process — #143–#146); unbounded allocations from hostile counts (
w:cols/@num,gridSpanautofit arrays, chart point counts, a list label built from an unboundedw:start— #147, #152–#155);NaNreaching the PDF content stream (#156). -
Fonts: unbounded glyph-buffer growth in shaping,
cmapsubtables that hang or OOM,ttcfheaders that pre-allocate gigabytes, raw unchecked table accessors (#157–#159, #181–#186). -
The XML DOM:
ReadPartAsXmlbuilds anXDocumentbounded only by the byte cap, so a small compressed part can amplify to roughly 2 GB of managed heap (#149). - No fuzzing yet. One hang test is the whole of the malformed-input corpus; a real fuzzing setup is filed and blocked on the hardening epics (#71).
Until the hardening epics close, treat conversion of untrusted documents as what it is — untrusted-input processing:
-
Always set
Limitsto the smallest bounds your documents genuinely need, and catchPackageTooLargeException. - Isolate hostile input. For a service converting documents from the public, run conversions in a worker process with an OS-level memory cap and a timeout, so an OOM, hang or stack overflow costs a worker rather than the service. This is sound advice for any document converter; it is stated here because this one tells you honestly why.
- A broken picture, chart or embedded font costs its own placement, not the conversion — expect documents to convert with pieces missing rather than fail, and decide whether that is acceptable for your use.
- Watch the register. The open issues state exactly what a hostile document can currently get; whether that risk is acceptable is your call to make with real information, which is the point of keeping the register public.
Findings are tracked openly as GitHub issues labelled security with a severity (sev:critical through sev:low). If you find something, an issue with a reproduction is the fastest path; each parser/decoder finding should state what a hostile .docx gets out of it, because that framing is what the register is organised around.
Using n8PDF
What it does
How it works
Contributing