Table of Contents

Class FontSafetyValidator

Namespace
NetPdf.Text.Fonts
Assembly
NetPdf.Text.dll

Per Phase C C-2 — pre-decode font safety gate. Routes incoming font bytes through a magic-byte sniffer + size + sfnt-header sanity check before they reach OpenTypeFont.Parse / HarfBuzz. Defends against:

  • HarfBuzz CVE-class bugs. CVE-2024-56732 type — malformed cmap / GSUB / GPOS / glyf tables triggering buffer overruns in the shaper. The validator can't fix the bug, but it bounds the attack surface by rejecting obviously-malformed sfnt headers before HarfBuzz sees the bytes.
  • DomPDF @font-face cache poisoning. Once Phase 5 wires @font-face, an attacker who can serve a font URL could pick the filename + content. The validator's format-magic check + size cap stop the cache from accepting non-fonts.

The validator does NOT replace the structural validation in OpenTypeFont.Parse — it just bounds the attack surface before parse runs. Parse is still authoritative for table-by-table integrity.

What this validator does NOT inspect: sfnt table tags (cmap / glyf / SVG / etc.) — only the numTables count + directory bounds. SVG-in-OpenType fonts are accepted by this gate; future hardening could walk the table tags to reject SVG-only fonts (a real attack surface) but that work is not in Phase C. Per PR #17 Copilot review #2 the docs were over-claiming; they now state actual coverage.

public static class FontSafetyValidator
Inheritance
FontSafetyValidator
Inherited Members

Fields

MaxBytes

Maximum encoded font bytes accepted by the pre-decode gate. Real-world fonts: Roboto Regular = 174 KiB, Noto Sans CJK Regular = 17 MiB. Cap at 32 MiB allows the largest legitimate single fonts + stops a 100 MiB attacker upload.

public const int MaxBytes = 33554432

Field Value

int

MaxTableCount

Maximum number of sfnt table records in the directory. Real fonts have 10–25 tables; 64 is generous. A 65 535-table file (the uint16 max) would exhaust pre-Parse buffer allocations.

public const int MaxTableCount = 64

Field Value

int

MinBytes

Minimum file size to even be considered. The sfnt header is 12 bytes + at least 16 bytes per table record; reject anything that can't fit a single-table sfnt.

public const int MinBytes = 28

Field Value

int

Methods

SniffFormat(ReadOnlySpan<byte>)

Identify the font format from bytes's leading 4 bytes. Caller is expected to have already checked bytes.Length >= 4.

public static FontSafetyValidator.FontFormat SniffFormat(ReadOnlySpan<byte> bytes)

Parameters

bytes ReadOnlySpan<byte>

Returns

FontSafetyValidator.FontFormat

Validate(ReadOnlySpan<byte>)

Validate fontBytes against the per-font safety caps. Two passes: byte-size check, then magic-byte sniff. For TrueType / OpenType-CFF formats, additionally validate the sfnt directory header (numTables ≤ MaxTableCount, header length consistent with fontBytes.Length).

public static FontSafetyValidator.ValidationResult Validate(ReadOnlySpan<byte> fontBytes)

Parameters

fontBytes ReadOnlySpan<byte>

Returns

FontSafetyValidator.ValidationResult

ValidateSfntHeader(ReadOnlySpan<byte>, FontFormat)

Per Phase D D-5 — sfnt directory walk. Beyond the header sanity (numTables + directory bounds), this validates EVERY table record's offset+length lies within the file + rejects any font that uses one of the danger-class tables NetPdf v1 doesn't render. The exact denylist enforced by IsDangerousTableTag(char, char, char, char) is: SVG (SVG-in-OpenType), sbix (Apple bitmap), CBDT + CBLC (Google color bitmap data + location), EBDT + EBLC (embedded bitmap data + location). SVG- in-OpenType has been a real attack surface (parsers process SVG payloads as graphics); bitmap glyph tables route through different rendering code we don't own. Rejecting them upfront keeps the attack surface bounded; a font with these tables alongside glyf/CFF will still render via the supported tables in a future relaxation.

 <p><b>Per PR #18 Copilot review #7 — table list aligned.</b>
 An earlier docstring revision listed <code>COLR</code>/<code>CPAL</code> as
 part of the denylist; those are color-glyph palette tables that
 NetPdf v1 ALSO doesn't render but the actual enforcement only
 covers the bitmap + SVG surfaces above. Decision: keep COLR/CPAL
 off the denylist for now (they're palette metadata, not embedded
 payloads — much smaller attack surface than SVG / bitmap glyph
 tables), and clarify the doc to match what's enforced. If a
 future review wants COLR/CPAL added too, both the doc + the
 <xref href="NetPdf.Text.Fonts.FontSafetyValidator.IsDangerousTableTag(System.Char%2cSystem.Char%2cSystem.Char%2cSystem.Char)" data-throw-if-not-resolved="false"></xref> body should be updated together.</p>

 <p><b>Per PR #18 review #9 — public for post-decompression
 re-validation.</b> WOFF / WOFF2 wrap an sfnt + apply zlib /
 Brotli compression to the table data. The pre-decompression
 validators (<xref href="NetPdf.Text.Fonts.FontSafetyValidator.ValidateWoffHeader(System.ReadOnlySpan%7bSystem.Byte%7d)" data-throw-if-not-resolved="false"></xref> /
 <xref href="NetPdf.Text.Fonts.FontSafetyValidator.ValidateWoff2Header(System.ReadOnlySpan%7bSystem.Byte%7d)" data-throw-if-not-resolved="false"></xref>) only see the wrapper, not the
 decoded sfnt directory. Phase 5's WOFF/WOFF2 decoder MUST call
 this method on the reconstructed sfnt bytes BEFORE handing them
 to <code>OpenTypeFont.Parse</code> / HarfBuzz — without that, a
 wrapped font with a <code>SVG </code> table sneaks past the table-tag
 denylist. Made <code>public</code> (was internal) so Phase 5 can
 invoke it from the decompressor without a back-channel.</p>
public static FontSafetyValidator.ValidationResult ValidateSfntHeader(ReadOnlySpan<byte> fontBytes, FontSafetyValidator.FontFormat format)

Parameters

fontBytes ReadOnlySpan<byte>
format FontSafetyValidator.FontFormat

Returns

FontSafetyValidator.ValidationResult