Enterprise edition
Content Disarm and Reconstruction — Deep Reference
At a glance
Section titled “At a glance”This page is the deep reference for the NextPDF\Enterprise\Security\Cdr module. The module disarms an untrusted PDF and reconstructs a clean file from its safe objects. The pipeline is: parse, admission control, threat detection, filtering, reference scrubbing, rebuild. The output is a security projection of the input, never an evidentiary copy. For workflow guidance, read the CDR capability page first.
Availability & licensing
Section titled “Availability & licensing”This capability ships in NextPDF Enterprise (nextpdf/enterprise) and activates with an Enterprise-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license.
Public API surface
Section titled “Public API surface”| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
CdrEngine::__construct | none | Constructs the internal detector and rebuilder | CdrEngine | Nothing declared | No injectable collaborators |
CdrEngine::sanitize | string $pdfData, ?CdrPolicy $policy = null | Runs the full pipeline under CdrPolicy::standard() | CdrResult | Does not throw on hostile input; parse and admission failures return a rejected result | The result reports rejection distinctly from sanitization |
CdrPolicy::__construct | seven optional named parameters, see fence | Empty removal set; allowUriActions false; flattenIncrementalUpdates true; limits 100000 objects, 256 MiB decoded, 10000 pages, 1000.0 inflation | CdrPolicy | Nothing declared | final readonly; an empty removeThreatTypes list detects nothing |
CdrPolicy::standard | none | Legacy threat set; URI actions removed; default limits | self | Nothing declared | Excludes the seven lossy Strip* cases |
CdrPolicy::paranoid | none | Legacy threat set with tighter limits: 50000 objects, 128 MiB, 5000 pages, 100.0 inflation | self | Nothing declared | Excludes the seven lossy Strip* cases |
CdrPolicy::permissive | none | Removes only JavaScript, LaunchAction, NamedJavaScript, SubmitForm, ImportData; preserves URI actions | self | Nothing declared | Intended for trusted sources |
CdrPolicy::allThreatTypes | none | Returns every ThreatType case, including the lossy Strip* cases | list<ThreatType> | Nothing declared | The explicit maximal-strip opt-in |
CdrPolicy::legacyThreatTypes | none | Returns every case except the seven Strip* cases | list<ThreatType> | Nothing declared | Default removal set for standard() and paranoid() |
CdrPolicy::shouldRemove | ThreatType $type | Membership test against removeThreatTypes | bool | Nothing declared | Returns false for UriAction when allowUriActions is true |
ThreatDetector::detect | PdfReader $reader, CdrPolicy $policy | Scans every object and the trailer catalog for the policy’s threat types | list<DetectedThreat> | Does not throw; an unparseable object becomes an UnparseableObject threat | Catalog scan covers the /Names/JavaScript tree |
CdrRebuilder::rebuild | PdfReader $reader, list<int> $safeObjNums, list<int> $removedObjNums, CdrPolicy $policy | Serializes safe objects into a single-revision %PDF-2.0 file | string | Nothing declared; objects that fail re-read or /Length validation are skipped | $policy is reserved for future serialization tweaks |
DetectedThreat::__construct | ThreatType $type, int $objectNumber, string $description, string $location = '' | Immutable finding value object | DetectedThreat | Nothing declared | All four properties are public readonly |
ThreatType | string-backed enum | Twenty cases: thirteen legacy plus seven opt-in Strip* cases | n/a | n/a | See the case inventory below |
Entry-point signatures
Section titled “Entry-point signatures”final class CdrEngine{ public function __construct()
public function sanitize(string $pdfData, ?CdrPolicy $policy = null): CdrResult}final readonly class CdrPolicy{ public function __construct( public array $removeThreatTypes = [], public bool $allowUriActions = false, public bool $flattenIncrementalUpdates = true, public int $maxObjects = 100_000, public int $maxDecodedStreamBytes = 268_435_456, public int $maxPageCount = 10_000, public float $maxInflationRatio = 1000.0, )
public static function standard(): self
public static function paranoid(): self
public static function permissive(): self
public static function allThreatTypes(): array
public static function legacyThreatTypes(): array
public function shouldRemove(ThreatType $type): bool}final class ThreatDetector{ public function detect(PdfReader $reader, CdrPolicy $policy): array}final class CdrRebuilder{ public function rebuild(PdfReader $reader, array $safeObjNums, array $removedObjNums, CdrPolicy $policy): string}final readonly class DetectedThreat{ public function __construct( public ThreatType $type, public int $objectNumber, public string $description, public string $location = '', )}enum ThreatType: stringThreatType case inventory
Section titled “ThreatType case inventory”Thirteen legacy cases form the default removal set. The Strip* cases are lossy by design and never enter a default policy.
| Case | Backing value | Detection surface |
|---|---|---|
ThreatType::JavaScript | javascript | /JS key on any object, or an /S /JavaScript action |
ThreatType::AdditionalActions | additional-actions | /AA dictionary on any object |
ThreatType::OpenAction | open-action | /OpenAction key on any object |
ThreatType::LaunchAction | launch-action | /S /Launch action |
ThreatType::RemoteGoTo | remote-goto | /S /GoToR or /S /GoToE action |
ThreatType::SubmitForm | submit-form | /S /SubmitForm action |
ThreatType::ImportData | import-data | /S /ImportData action |
ThreatType::EmbeddedFiles | embedded-files | /EmbeddedFiles name tree or /EF dictionary |
ThreatType::RichMedia | rich-media | /Subtype /RichMedia |
ThreatType::NamedJavaScript | named-javascript | Catalog /Names/JavaScript name tree |
ThreatType::UriAction | uri-action | /S /URI action; suppressed when allowUriActions is true |
ThreatType::Xfa | xfa | /XFA key |
ThreatType::UnparseableObject | unparseable-object | Any object or catalog that fails parsing |
ThreatType::StripJavaScript | strip-javascript | Opt-in superset: /JS key, /S /JavaScript, or /Subtype /JavaScript |
ThreatType::StripEmbeddedFiles | strip-embedded-files | Opt-in: /Type /EmbeddedFile, /Type /Filespec, /EmbeddedFiles, or /EF |
ThreatType::StripFormFields | strip-form-fields | Opt-in: /Subtype /Widget, /FT key, or /AcroForm key |
ThreatType::StripAnnotationsRich | strip-annotations-rich | Opt-in subtypes: Movie, Sound, FileAttachment, 3D, RichMedia, Screen |
ThreatType::StripOcgNonDefault | strip-ocg-non-default | Opt-in: /Type /OCG with a /Usage or /Visibility key |
ThreatType::StripDigitalSignaturesAtRebuild | strip-digital-signatures-at-rebuild | Opt-in: /Type /Sig, /FT /Sig, /DSS, /VRI, or /ByteRange |
ThreatType::Strip3dAndRichMedia | strip-3d-and-rich-media | Opt-in subtypes: 3D, U3D, PRC, RMF, RichMedia, Sound, Movie |
Behavior contract
Section titled “Behavior contract”CdrEngine::sanitize executes six ordered phases and never throws for hostile input.
- Parse. A parse failure returns a result with
admittedfalse and a parse-error rejection reason. The sanitized output is empty in that case. - Admission control. The object count, aggregate decoded stream bytes, per-stream inflation ratio, and page count are checked against the policy limits. An over-limit document is rejected, not sanitized. Rejection and sanitization are reported distinctly.
- Detection.
ThreatDetector::detectscans every object and the trailer catalog for the policy’s threat types. Unparseable objects are recorded asThreatType::UnparseableObjectfindings rather than skipped. - Filtering. Objects carrying findings are queued for removal. The document catalog is never removed as a whole object. Catalog-level findings (
OpenAction,AdditionalActions,NamedJavaScript) are remediated by key stripping instead. - Reference scrubbing. Every indirect reference to a removed object is replaced with
nullduring serialization. - Rebuild.
CdrRebuilder::rebuildemits a single-revision%PDF-2.0file with renumbered objects, a classic cross-reference table, and a fresh trailer. Safe stream bytes are copied byte-identically. The rebuilt catalog drops/OpenAction,/AA, and/Names;/AAis dropped from every object.
The returned CdrResult exposes the rebuilt bytes, the removed-threat list, both byte sizes, the admission flag, and the rejection reason. If the source had a resolvable /Root and the rebuilt output lost it, the engine rejects the output instead of returning a structurally broken file. This is a fail-closed guarantee: admitted true implies the output still carries a document catalog reference.
Incremental updates never survive: the rebuild serializes exactly one revision under every policy, so shadow-style late revisions are flattened by construction. Original digital signatures cannot remain valid across a rebuild, because byte ranges no longer match the output.
Architecture red line. CDR is a security projection layer, not a preservation layer. The output must not be used for legal evidence preservation, hash comparison with the original, or archival copies.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- A
nullpolicy resolves toCdrPolicy::standard(). A policy constructed with the default emptyremoveThreatTypesdetects and removes nothing. allowUriActionssettruesuppressesUriActionremoval even when the case is present inremoveThreatTypes.flattenIncrementalUpdatesis declarative in this release: the rebuild emits a single revision under every policy, includingpermissive(), which sets the flagfalse.- The inflation-ratio check treats a raw stream length of zero as one, so a stream inflating from nothing is still bounded. When no decoded form is retained, the raw stream length counts toward the aggregate budget.
- The page-count admission check is best-effort: a catalog or page-tree read failure does not reject the document by itself. The object-count and decompression budgets are always enforced.
- An object whose raw stream length disagrees with its integer
/Lengthentry is skipped at rebuild time (polyglot defense). A reference to such a skipped object retains its source object number and may not resolve in the output.sanitize()refuses detectably broken results (a missing/Root), but a caller driving the low-levelCdrRebuilder::rebuild()directly must re-validate output structure and reference integrity itself. - When the source trailer carries
/ID, the rebuilt trailer carries a freshly generated random/ID, not the original. Other trailer entries, including/Info, are not carried over; the rebuilt trailer holds/Size,/Rootwhen resolvable, and the regenerated/ID. - Decoded name and key bytes are re-emitted with hexadecimal escapes for delimiters, whitespace, and non-printable bytes, so hostile names cannot inject dictionary syntax into the output.
- String values under dictionary keys outside the known name-valued set are conservatively emitted as literal strings.
CdrPolicy::legacyThreatTypes()treats any future enum case as default-removed unless it is registered as aStrip*case, so new lossy cases cannot silently enter default policies.- CDR is not a cryptographic module. Its only randomness use is the regenerated trailer
/ID. Signature validation is out of scope here; see the Signature deep reference.
Conformance
Section titled “Conformance”| Claim | Standard | Clause |
|---|---|---|
| Invoking an ECMAScript action makes a PDF processor execute the embedded script. | ISO 32000-2 | §12.6.4.17 |
Document-level scripts in the JavaScript name tree all execute when the document opens. | ISO 32000-2 | §12.6.4.17 |
The catalog name dictionary may hold a JavaScript name tree of document-level script actions. | ISO 32000-2 | §7.7.4 (Table 32) |
| A launch action launches an application, or opens or prints a document. | ISO 32000-2 | §12.6.4.6 |
/AA additional-actions dictionaries extend the trigger events on annotations, pages, fields, and the catalog. | ISO 32000-2 | §12.6.3 |
| Untrusted-file intake must bound the presence, volume, and content of incoming files. | OWASP ASVS 5.0 | §5.2 |
| Systems should prevent inappropriate execution of uploaded files and detect dangerous content. | OWASP ASVS 5.0 | §5.3 |
All clauses are paraphrased; NextPDF does not reproduce normative text. CDR removes the active-content surfaces enumerated by ThreatType under the configured policy. CDR is not an antivirus scanner and does not detect malware signatures; it complements controls such as OWASP ASVS 5.4.3 antivirus scanning. Whether a disarmed file is acceptable for a given intake pipeline remains the operator’s risk decision.
Development notes
Section titled “Development notes”- The module source carries
@since 1.9.0; this reference documents the surface as shipped innextpdf/enterprise3.1.0. - Everything runs in process on your host. No network access occurs during sanitization.
CdrPolicyandDetectedThreatarefinal readonly; construct a new policy instance to change limits.CdrEngineconstructs its detector and rebuilder internally.ThreatDetectorandCdrRebuilderremain directly usable for staged pipelines that supply their ownPdfReader.- The
$policyparameter ofCdrRebuilder::rebuildis presently reserved; the source documents it as kept for call-site compatibility and future per-policy serialization tweaks. - Output is structurally reproducible, not bitwise reproducible: the regenerated
/IDdiffers on every run when the source carried one. - The result type
CdrResult(return value ofsanitize()) is covered behaviorally above; its fields arepublic readonly, withhadThreats()andthreatCount()as conveniences.
See also
Section titled “See also”- Content Disarm and Reconstruction (CDR) — the capability page with workflow and policy guidance.
- Security — Deep Reference
- Validation — Deep Reference
- Forensics — Deep Reference
Publication boundary
Section titled “Publication boundary”This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.