Skip to content
getnextpdf.com

Enterprise edition

Content Disarm and Reconstruction — Deep Reference

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.

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.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
CdrEngine::__constructnoneConstructs the internal detector and rebuilderCdrEngineNothing declaredNo injectable collaborators
CdrEngine::sanitizestring $pdfData, ?CdrPolicy $policy = nullRuns the full pipeline under CdrPolicy::standard()CdrResultDoes not throw on hostile input; parse and admission failures return a rejected resultThe result reports rejection distinctly from sanitization
CdrPolicy::__constructseven optional named parameters, see fenceEmpty removal set; allowUriActions false; flattenIncrementalUpdates true; limits 100000 objects, 256 MiB decoded, 10000 pages, 1000.0 inflationCdrPolicyNothing declaredfinal readonly; an empty removeThreatTypes list detects nothing
CdrPolicy::standardnoneLegacy threat set; URI actions removed; default limitsselfNothing declaredExcludes the seven lossy Strip* cases
CdrPolicy::paranoidnoneLegacy threat set with tighter limits: 50000 objects, 128 MiB, 5000 pages, 100.0 inflationselfNothing declaredExcludes the seven lossy Strip* cases
CdrPolicy::permissivenoneRemoves only JavaScript, LaunchAction, NamedJavaScript, SubmitForm, ImportData; preserves URI actionsselfNothing declaredIntended for trusted sources
CdrPolicy::allThreatTypesnoneReturns every ThreatType case, including the lossy Strip* caseslist<ThreatType>Nothing declaredThe explicit maximal-strip opt-in
CdrPolicy::legacyThreatTypesnoneReturns every case except the seven Strip* caseslist<ThreatType>Nothing declaredDefault removal set for standard() and paranoid()
CdrPolicy::shouldRemoveThreatType $typeMembership test against removeThreatTypesboolNothing declaredReturns false for UriAction when allowUriActions is true
ThreatDetector::detectPdfReader $reader, CdrPolicy $policyScans every object and the trailer catalog for the policy’s threat typeslist<DetectedThreat>Does not throw; an unparseable object becomes an UnparseableObject threatCatalog scan covers the /Names/JavaScript tree
CdrRebuilder::rebuildPdfReader $reader, list<int> $safeObjNums, list<int> $removedObjNums, CdrPolicy $policySerializes safe objects into a single-revision %PDF-2.0 filestringNothing declared; objects that fail re-read or /Length validation are skipped$policy is reserved for future serialization tweaks
DetectedThreat::__constructThreatType $type, int $objectNumber, string $description, string $location = ''Immutable finding value objectDetectedThreatNothing declaredAll four properties are public readonly
ThreatTypestring-backed enumTwenty cases: thirteen legacy plus seven opt-in Strip* casesn/an/aSee the case inventory below
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: string

Thirteen legacy cases form the default removal set. The Strip* cases are lossy by design and never enter a default policy.

CaseBacking valueDetection surface
ThreatType::JavaScriptjavascript/JS key on any object, or an /S /JavaScript action
ThreatType::AdditionalActionsadditional-actions/AA dictionary on any object
ThreatType::OpenActionopen-action/OpenAction key on any object
ThreatType::LaunchActionlaunch-action/S /Launch action
ThreatType::RemoteGoToremote-goto/S /GoToR or /S /GoToE action
ThreatType::SubmitFormsubmit-form/S /SubmitForm action
ThreatType::ImportDataimport-data/S /ImportData action
ThreatType::EmbeddedFilesembedded-files/EmbeddedFiles name tree or /EF dictionary
ThreatType::RichMediarich-media/Subtype /RichMedia
ThreatType::NamedJavaScriptnamed-javascriptCatalog /Names/JavaScript name tree
ThreatType::UriActionuri-action/S /URI action; suppressed when allowUriActions is true
ThreatType::Xfaxfa/XFA key
ThreatType::UnparseableObjectunparseable-objectAny object or catalog that fails parsing
ThreatType::StripJavaScriptstrip-javascriptOpt-in superset: /JS key, /S /JavaScript, or /Subtype /JavaScript
ThreatType::StripEmbeddedFilesstrip-embedded-filesOpt-in: /Type /EmbeddedFile, /Type /Filespec, /EmbeddedFiles, or /EF
ThreatType::StripFormFieldsstrip-form-fieldsOpt-in: /Subtype /Widget, /FT key, or /AcroForm key
ThreatType::StripAnnotationsRichstrip-annotations-richOpt-in subtypes: Movie, Sound, FileAttachment, 3D, RichMedia, Screen
ThreatType::StripOcgNonDefaultstrip-ocg-non-defaultOpt-in: /Type /OCG with a /Usage or /Visibility key
ThreatType::StripDigitalSignaturesAtRebuildstrip-digital-signatures-at-rebuildOpt-in: /Type /Sig, /FT /Sig, /DSS, /VRI, or /ByteRange
ThreatType::Strip3dAndRichMediastrip-3d-and-rich-mediaOpt-in subtypes: 3D, U3D, PRC, RMF, RichMedia, Sound, Movie

CdrEngine::sanitize executes six ordered phases and never throws for hostile input.

  1. Parse. A parse failure returns a result with admitted false and a parse-error rejection reason. The sanitized output is empty in that case.
  2. 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.
  3. Detection. ThreatDetector::detect scans every object and the trailer catalog for the policy’s threat types. Unparseable objects are recorded as ThreatType::UnparseableObject findings rather than skipped.
  4. 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.
  5. Reference scrubbing. Every indirect reference to a removed object is replaced with null during serialization.
  6. Rebuild. CdrRebuilder::rebuild emits a single-revision %PDF-2.0 file 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; /AA is 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.

  • A null policy resolves to CdrPolicy::standard(). A policy constructed with the default empty removeThreatTypes detects and removes nothing.
  • allowUriActions set true suppresses UriAction removal even when the case is present in removeThreatTypes.
  • flattenIncrementalUpdates is declarative in this release: the rebuild emits a single revision under every policy, including permissive(), which sets the flag false.
  • 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 /Length entry 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-level CdrRebuilder::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, /Root when 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 a Strip* 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.
ClaimStandardClause
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.

  • The module source carries @since 1.9.0; this reference documents the surface as shipped in nextpdf/enterprise 3.1.0.
  • Everything runs in process on your host. No network access occurs during sanitization.
  • CdrPolicy and DetectedThreat are final readonly; construct a new policy instance to change limits.
  • CdrEngine constructs its detector and rebuilder internally. ThreatDetector and CdrRebuilder remain directly usable for staged pipelines that supply their own PdfReader.
  • The $policy parameter of CdrRebuilder::rebuild is 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 /ID differs on every run when the source carried one.
  • The result type CdrResult (return value of sanitize()) is covered behaviorally above; its fields are public readonly, with hadThreats() and threatCount() as conveniences.

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.