Class FontSafetyValidator
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/glyftables 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
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
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
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
bytesReadOnlySpan<byte>
Returns
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
fontBytesReadOnlySpan<byte>
Returns
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
fontBytesReadOnlySpan<byte>formatFontSafetyValidator.FontFormat