-
Notifications
You must be signed in to change notification settings - Fork 0
Threat Model
This page is the artifact a "is this a security bug?" call is measured against. The Security
page tells the story of what has been defended and where the register stands; this one states the
boundary, the surfaces, the sinks, and the guarantee plainly enough that a future finding can be
judged against it rather than against a feeling. Where the two disagree, the live
security label is the
truth.
n8PDF is a library. One assembly is added to a consumer's program and one method is called:
Converter.Convert(docxStream, pdfStream, options);Everything the consumer's own code decides — which stream to hand in, which ConversionOptions to
set, where the output goes — is trusted. Everything that arrives inside the .docx stream
is attacker-controlled: n8PDF exists to read a document someone else wrote, and a file written
to be hostile lies about itself in every field it can.
The boundary is the .docx stream. The threat model is about what crosses it.
trusted | attacker-controlled
------------------------------- | ----------------------------------------
the calling program | the .docx bytes and everything in them:
ConversionOptions / Limits | the ZIP container and its headers
the output stream | every XML part
the host's own fonts on disk | every embedded image, font, metafile
| every count, offset, length, and depth
The host's installed fonts, and any face the caller registers into a FontLibrary, are trusted
input — they come from the machine, not the document. A font that arrives embedded in the
document (w:embedRegular and its siblings) is not: since embedded fonts landed, the whole of
Fonts/ reads attacker-controlled bytes.
Every one of these is parsed, from scratch, by code in this repository — there is no third-party DOCX or PDF library, no headless Word, no browser engine to inherit a hardening story from. Each is a surface the audit treats as hostile:
| Surface | What arrives | Parsed by |
|---|---|---|
| ZIP container | part sizes and offsets in the local and central headers — described wrongly by a hostile file | Packaging/ |
| XML parts | the document, styles, numbering, settings, charts, diagrams — counts, references, and (if permitted) entity definitions |
Ooxml/, read through a DTD-prohibiting reader |
| Images | PNG, GIF, BMP, TIFF (LZW, PackBits, Deflate, CCITT fax; strips, tiles, planar), EMF/EMF+ metafiles, JPEG (sequential, progressive, arithmetic) — each declaring its own dimensions and sample counts | Images/ |
| Embedded fonts | obfuscated SFNT/OpenType/CFF/AAT tables with their own offsets, lengths, and glyph counts | Fonts/ |
| Charts, equations, diagrams | data-point counts, series, nesting, and recursion of their own |
Layout/, Ooxml/
|
A hostile value is only dangerous where it reaches something that acts on its magnitude. The sinks this project guards, and against which a finding is judged, are:
-
Allocation size — a count, length, or dimension read off the wire and used to size a buffer
before the data behind it is read. A 57-byte PNG can declare fifty thousand pixels square; a
malformed SFNT can declare a two-gigabyte table. Bounds are counted against what actually comes
out of a decompressor, in
longarithmetic where anintproduct would overflow. -
Array index / offset — a value used to reach into a buffer. The guard form is
pos > length - count, neverpos + count > length, so the bound itself cannot overflow before it is checked; and since #266 the whole library compiles with overflow checking, so an integer that wraps on the way to an index becomes a catchableOverflowExceptionrather than a silent negative. -
Recursion depth — nested tables, text boxes, equation markup, inline wrappers, nested
metafiles. Unbounded recursion is a stack overflow, which in .NET ends the process outright
and cannot be caught, so depth is bounded explicitly (
ParseGuard, image nesting limits). - Unbounded work — a crafted count or structure that turns a linear pass quadratic, or a loop that never terminates. These exhaust CPU rather than memory; they are bounded by structure or by a wall-clock assertion in the suite.
Because n8PDF is pure managed .NET with no native code and no unsafe, a value out of bounds
is a thrown exception, not a write past the end of a buffer. There is no path from a hostile
document to arbitrary code execution or memory disclosure through this library's own code; the
runtime's own memory safety stands underneath it.
So the realistic failure modes — the ones the audits look for, and the only ones a report should expect to be treated as vulnerabilities — are availability-shaped:
- Unbounded memory (OOM) — an allocation sized from a hostile value.
- Unbounded work (a hang) — CPU that does not terminate in time.
- A stack overflow — unbounded recursion, which ends the process.
- An uncaught runtime exception — one that escapes the decoders' own handling and the conversion-level net, aborting a conversion (or, if it reaches the caller as an undocumented type, breaking the API contract).
This is a statement about this library's code. It is not a claim about the .NET runtime, the operating system, or the consumer's own program — and it does not retire the deployment guidance below.
A report is a security finding when a crafted .docx (or a part inside one) drives n8PDF to
one of the four failure modes above — memory exhaustion, a hang, a process-ending stack overflow,
or an uncaught/undocumented exception out of the public API. State what the hostile document gets:
that framing is how the register is organised.
A report is not a security finding when:
- The output is merely wrong or missing — a picture, chart, or embedded font that could not be read is left out, the conversion going on without it, by design. That is a fidelity or functionality matter (see Functionality and Known Gaps), not a vulnerability.
- The failure comes from trusted input — the calling program, the options, the output stream, or a font on the host — rather than from the document bytes.
- A documented exception type is thrown for a malformed document. The contract is that a malformed document throws only documented types (never a raw runtime crash) and a malformed image returns null; a documented throw is the contract being kept.
-
OutOfMemoryExceptionis observed and not swallowed. The conversion-level net deliberately does not catch it — that one means the process is in trouble and hiding it helps nobody.
When in doubt, report it privately (see SECURITY.md) and let the assessment make the call against this page.
The guarantee above is about what the library promises; a service that converts documents from the public should still assume the attack nobody has found yet:
-
Set
ConversionOptions.Limitsto the smallest bounds your documents need, and catchPackageTooLargeException. - Run conversions in an isolated worker with an OS-level memory cap and a timeout, so an uncatchable failure (a stack overflow, an OOM) costs a worker rather than the service.
- Expect pieces missing rather than a failed conversion, and decide whether that is acceptable.
Vulnerabilities are reported privately — see
SECURITY.md for the process and what to
include. Non-security bugs are public issues — see Reporting Bugs. The register itself is the
open security label.
Using n8PDF
What it does
How it works
Contributing