XRef tables bypass configured limits
GHSA-fjh6-rrhv-4g63 · CVE assignment pending
Summary
pdfcpu enforces configured cross-reference resource limits (MaxObjectCount, MaxXRefEntries) when parsing XRef streams (PDF 1.5+), but the legacy code path for traditional XRef tables (PDF 1.4 and earlier) never validates the subsection object count. A crafted PDF declaring a huge objCount with matching entries forces the cross-reference map to grow without bound, exhausting process memory and crashing any host application.
Impact
- Denial of Service via memory exhaustion — process OOM or severe degradation.
- Attack vector: network — malicious PDF via upload or attachment.
- Bypass of intended control: the resource-limit configuration is silently not applied on the legacy path.
- Scope: any application using pdfcpu to process untrusted PDFs.
Root Cause
In pkg/pdfcpu/read.go, parseXRefTableSubSection() reads the subsection object count with strconv.Atoi and never checks it against the configured limits. Each valid xref entry then inserts into the cross-reference map, which grows unbounded:
// pkg/pdfcpu/read.go:445 — no limit check
objCount, err := strconv.Atoi(fields[1])
// read.go:424 — every entry grows the map
xRefTable.Table[objNr] = &entry
By contrast, XRef streams enforce MaxObjectCount = 10M in xRefStreamSize() (model/parse.go) — the traditional-table path simply lacks the equivalent check.
Proof of Concept
A crafted PDF with a traditional XRef table:
xref
0 100000000 ← objCount = 100M, never validated
0000000000 65535 f
0000000010 00000 n
... ← attacker supplies matching entries
trailer << /Size 100000001 /Root 1 0 R >>
Each ~30-byte entry creates a map entry costing ~128 bytes of Go map overhead. 100M entries → multi-GB memory growth → OOM kill. Mitigating constraints: the loop stops on the first line without 3 fields, and the attacker must provide enough well-formed entries to match objCount, so the attack requires a large (GB-scale) file — but memory amplification is roughly 4×.
Disclosure Timeline
| Date | Event |
|---|---|
| 2026-09-18 | Report submitted via GitHub Private Vulnerability Reporting |
| 2026-09-18 | Accepted by maintainer; retitled "Traditional cross-reference tables bypass configured XRef limits" |
| 2026-09-28 | Advisory published — GHSA-fjh6-rrhv-4g63 |
| Fixed in | pdfcpu v0.16.0 |
Fix
Validate objCount against MaxObjectCount on the traditional-table path, mirroring the XRef-stream enforcement:
// After (safe):
objCount, err := strconv.Atoi(fields[1])
if err != nil {
return "", err
}
if objCount > limits.MaxObjectCount {
return "", fmt.Errorf("xref subsection objCount %d exceeds limit %d",
objCount, limits.MaxObjectCount)
}
Fixed in pdfcpu v0.16.0. Advisory published 2026-09-28; CVE assignment pending.