Skip to content

Threat Model

npond edited this page Aug 26, 2026 · 1 revision

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.

The boundary

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.

The attacker-controlled surfaces

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/

The sinks that matter

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 long arithmetic where an int product would overflow.
  • Array index / offset — a value used to reach into a buffer. The guard form is pos > length - count, never pos + 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 catchable OverflowException rather 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.

The guarantee

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.

Deciding whether a finding is a security bug

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.
  • OutOfMemoryException is 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.

Defence in depth for a deployment

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:

  1. Set ConversionOptions.Limits to the smallest bounds your documents need, and catch PackageTooLargeException.
  2. 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.
  3. Expect pieces missing rather than a failed conversion, and decide whether that is acceptable.

Reporting

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.

Clone this wiki locally