This review documents security checks performed on the PHP generator and its locked dependencies: code review, regression tests, dependency audit and PDF validation by independent readers. It covers PDF value serialization, inline images, binary resources, graphs, numeric limits and file writing. The results below concern the verified surfaces and scenarios; they are not a guarantee of the absence of every vulnerability.

TrueType and PNG/JPEG converter checks are now maintained in the optional pdf-fonts and pdf-images modules. PDF objects and their invariants remain in the core. The pdf-reader module has its own security documentation for reading external files; this generator review does not replace it.

Results and protections

Surface Review result
Names, strings, numbers, header Separators and bytes are escaped, literal strings preserve data, numbers are finite and header versions are limited by an enum. Payloads attempting to close a string or introduce other objects remain data.
PDF streams /Length is calculated from encoded bytes. endstream markers within data do not change this length. StreamObject rejects external /F, /FFilter and /FDecodeParms entries.
Inline images An EI ambiguity was corrected: a tolerant reader may recognize the terminator without preceding whitespace. Bytes could then be read as content operators. Such sequences are now rejected; an image XObject provides a separate representation with an explicit length.
Image dimensions Checked integer calculation of row bytes and the total. Overflow is rejected rather than passing through a float and an incorrect length.
PNG/JPEG import — pdf-images module Explicitly supplied bytes, without path/URL loading. Limits on bytes, pixels, dimensions and segments; PNG CRC/order/filters/bounded zlib; JPEG markers, tables and colors inspected. The PDF reader decodes JPEG coefficients.
TrueType program — pdf-fonts module Validation of explicitly supplied binary data: tables, offsets, metrics, cmap and glyphs checked; composite-glyph cycles and depth bounded. Unsupported CFF, variable fonts and collections are rejected.
Shared operands Validation visits each identity once, with a separate set for active ancestors. It preserves cycle rejection without exponentially revisiting a shared subgraph.
Cross-references Initial gaps receive free records as required by the standard. CrossReferenceLimits bounds cumulative capacity before allocation. Incremental sections remain partial; additions for /Size and the xref stream number are checked.
Revisions Old-number reservations, preserved generations and aliases; structural-container replacement rejected even through a new object stream. Catalog and page-tree metadata are checked against effective values. Old bytes remain in the file: an update cannot erase a secret.
File output Path validation before serialization, rejection of stream URIs, write exception without path disclosure. A graph validation error occurs before opening an existing destination.
Dependencies No known advisory or abandoned package reported by composer audit --locked during the review. This check runs in every CI build.
Historical Adobe R=5 encryption Official AES vectors, qpdf fixtures and independent string/stream decryption, with user/owner passwords and incorrect-password rejection. Checks of lengths after filtering and encryption, absence of double encryption and structural exceptions.
Password preparation Fixed Unicode 3.2 SASLprep/NFKC, compared with an independent oracle: mappings, prohibited and unassigned characters, bidirectional rules and truncation after normalization.
ISO R=2/3/4 encryption Algorithms 1–7, user/owner credentials, RC4 and AESV2, 40–128-bit keys, implicit and explicit filters and incremental revisions. Explicit Crypt filters use their key directly according to 7.4.10; implicit streams use per-object derivation.
Certificate encryption CMS messages, recipient grouping, s3/s4/s5 formats and distinct document and attachment keys. Independent verification of CMS data, keys and decryption. Permission byte order is an explicit choice.
Encrypted-writer boundary Direct trailer strings outside ID rejected; Encrypt dictionary and xref remain clear. Only catalog metadata may be exempted; replacement follows the revision's effective binding. A new PHP identity cannot replace Encrypt inside an object stream.
Signatures ByteRange fixed before signing, Contents reservation with explicit capacity, delimiter checking and filling without shifting offsets. The three ISO formats undergo independent verification of digests, CMS messages and RSA signatures.

Explicit stream-URI rejection hardens the writeTo() contract rather than correcting demonstrated PHP execution: LOCK_EX already blocked common wrappers on the verified PHP 8.5 runtime. A native destination may still follow a symbolic link or point to a network share depending on the system. The application must control its location and permissions.

Using data in the right place

ShowText(new LiteralString($text)) escapes string syntax. NameType($name) escapes name syntax. However, StreamObject($rawContent) writes supplied bytes: when used as /Contents, they are PDF drawing code. Use ContentStreams and its typed operators to compose content from external text.

A PDF can represent a JavaScript action, launch action or URI. The library does not execute them in PHP and retains these definitions because they belong to the format. A reader may interpret them later. An application accepting user choices must allowlist action types and destinations rather than freely accepting an arbitrary graph or entries.

EncodedData passes already-encoded bytes. It does not analyze or decompress an incoming PNG, JPEG or PDF. The PngImageDecoder and JpegImageReader in pdftoolkit/pdf-images perform their dedicated validations and receive explicit limits; they do not replace an upload service's quotas or access controls. The core's DCTImage accepts an explicitly declared DCT representation without inspecting its markers. Font programs are also binary data; their analysis belongs to pdftoolkit/pdf-fonts. Hinting engines and DCT decoding remain executed by the reader opening the document.

Writing or serving a document

The application must choose the writeTo() path in a controlled location. The method overwrites an existing destination and does not create its parent directory. It is neither a path sandbox nor an atomic write: a hardware failure during writing may leave an incomplete file.

For an HTTP response, the application can send toPdf() bytes with its own access controls and appropriate headers:

$bytes = $document->toPdf();
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="document.pdf"');
header('X-Content-Type-Options: nosniff');
echo $bytes;

The download filename is fixed here. Request data must not be interpolated directly into headers or paths. Repository examples are CLI scripts; their first argument is a path selected by the person running the command.

Availability and coverage

Numeric checks prevent identified overflows; operand validation avoids unnecessary revisits. They do not replace service quotas. A shared direct graph can represent very large output when expanded into PDF syntax. A filter chain can also greatly increase byte counts. An input-size limit alone therefore does not bound all serialization.

SecuritySerializationTest, SecurityLimitsTest and FileOutputTest cover injection payloads, all 256 byte values, inline terminators, numeric bounds, shared graphs and output. ImageObjectsTest, CrossReferenceSectionTest and IncrementalUpdateTest cover core PDF invariants. Image-decoding regressions are in the module's ImageImportTest. TrueType validation remains in font module tests. Independent pypdf reads confirmed binary vectors and reproduction of the inline ambiguity. qpdf checks every example in CI.

This review is not an audit of PDF readers, PHP, the HTTP server or a future layout library. Generated definition files express standard constraints; they are not a general document-sanitization policy.

Cross-review of new mechanisms identified two defects in development versions: a metadata exemption attached to an old identity and replacement of Encrypt by an object-stream member. EncryptedRevisionInvariantTest checks their corrections, including actually written XML content.

The ISO, certificate and signature validators are development dependencies with locked versions and hashes. Every oracle states its reader's limitations: qpdf 12.4.2 also derives a per-object key for explicit Crypt; pyHanko verifies the ISO rule without modifying its algorithms. For some older RC4 variants, pypdf uses its own general provider to accept every permitted integer length.

A mathematical signature does not establish trust in its signer. Certificate policy, revocation, timestamping and interpretation of permitted changes require separate checks. Private keys in fixtures are public and exclusively intended for tests.

The native backend checks table 257 algorithms and key sizes against the effective PDF version. CMS messages use SHA-256; the PHP OpenSSL API used does not provide a full digest selection. SignatureFieldSigning orchestration applies actually written seeds, locks, certificates and appearances, then mathematically verifies the result before filling the reservation. An unavailable digest or evidence requirement remains rejected. Simple signing rejects policies that require this dedicated contract. A certificate chain declared for a PKCS#1 signature must start with the certificate actually used by this backend.

The output verification, field-policy and historical-permission tests cover altered output, mismatched certificates, replayed facts, locks and permissions. The policy oracle verifies eight PDFs and nine RSA/DSA signatures without modifying readers. General comparison of MDP/UR-permitted modifications remains separate.

AdobeEncryptionTest and validate_adobe_encryption.py cover historical R=5. This protocol is explicitly selected for interoperability: its password derivation is old and AES-CBC does not authenticate content. SASLprep preparation does not strengthen a weak password. The Adobe guide states the mechanisms actually available and their limits.

Policy and private reporting · Architecture · Standard coverage

Guide source at the documented commit ↗