跳转到内容
getnextpdf.com

Enterprise 版本

内容解除与重构 — 深度参考

本页是 NextPDF\Enterprise\Security\Cdr 模块的深度参考。该模块会解除一份不可信 PDF 的武装,并从其安全对象重构出一个干净文件。流水线为:解析、准入控制、威胁检测、过滤、引用清除、重构。输出是输入的安全投影,绝不是取证副本。如需工作流指引,请先阅读 CDR 能力页面

此能力随 NextPDF Enterprisenextpdf/enterprise)一同发布,并通过 Enterprise 层级的许可信封激活。没有该授权的部署不会加载此能力的类。比较各版本并获取许可证

符号参数默认行为返回抛出或失败于说明
CdrEngine::__construct构造内部的检测器与重构器CdrEngine未声明任何抛出无可注入的协作者
CdrEngine::sanitizestring $pdfData?CdrPolicy $policy = nullCdrPolicy::standard() 下运行完整流水线CdrResult对敌意输入不抛出异常;解析与准入失败会返回被拒绝的结果结果将拒绝与清洗区分报告
CdrPolicy::__construct七个可选具名参数,见代码块空移除集合;allowUriActions 为 false;flattenIncrementalUpdates 为 true;限制为 100000 个对象、256 MiB 解码、10000 页、1000.0 膨胀比CdrPolicy未声明任何抛出final readonly;空的 removeThreatTypes 列表不会检测任何内容
CdrPolicy::standard传统威胁集合;移除 URI 动作;默认限制self未声明任何抛出不包含七个有损的 Strip* 枚举项
CdrPolicy::paranoid传统威胁集合搭配更紧的限制:50000 个对象、128 MiB、5000 页、100.0 膨胀比self未声明任何抛出不包含七个有损的 Strip* 枚举项
CdrPolicy::permissive仅移除 JavaScriptLaunchActionNamedJavaScriptSubmitFormImportData;保留 URI 动作self未声明任何抛出面向可信来源
CdrPolicy::allThreatTypes返回每一个 ThreatType 枚举项,包括有损的 Strip*list<ThreatType>未声明任何抛出显式的最大化剥离选择项
CdrPolicy::legacyThreatTypes返回除七个 Strip* 项之外的每一个枚举项list<ThreatType>未声明任何抛出standard()paranoid() 的默认移除集合
CdrPolicy::shouldRemoveThreatType $type针对 removeThreatTypes 的成员测试bool未声明任何抛出allowUriActionstrue 时,对 UriAction 返回 false
ThreatDetector::detectPdfReader $readerCdrPolicy $policy扫描每个对象以及尾部目录(trailer catalog)以查找策略指定的威胁类型list<DetectedThreat>不抛出;无法解析的对象会变成一个 UnparseableObject 威胁目录扫描覆盖 /Names/JavaScript
CdrRebuilder::rebuildPdfReader $readerlist<int> $safeObjNumslist<int> $removedObjNumsCdrPolicy $policy将安全对象序列化为一个单修订版 %PDF-2.0 文件string未声明任何抛出;重新读取或 /Length 校验失败的对象会被跳过$policy 为未来的序列化调整保留
DetectedThreat::__constructThreatType $typeint $objectNumberstring $descriptionstring $location = ''不可变的发现值对象DetectedThreat未声明任何抛出四个属性均为 public readonly
ThreatType字符串支撑的枚举二十个枚举项:十三个传统项加七个可选加入的 Strip*不适用不适用参见下方枚举清单
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

十三个传统项构成默认移除集合。Strip* 项在设计上是有损的,永远不会进入任何默认策略。

枚举项支撑值检测面
ThreatType::JavaScriptjavascript任意对象上的 /JS 键,或一个 /S /JavaScript 动作
ThreatType::AdditionalActionsadditional-actions任意对象上的 /AA 字典
ThreatType::OpenActionopen-action任意对象上的 /OpenAction
ThreatType::LaunchActionlaunch-action/S /Launch 动作
ThreatType::RemoteGoToremote-goto/S /GoToR/S /GoToE 动作
ThreatType::SubmitFormsubmit-form/S /SubmitForm 动作
ThreatType::ImportDataimport-data/S /ImportData 动作
ThreatType::EmbeddedFilesembedded-files/EmbeddedFiles 名称树或 /EF 字典
ThreatType::RichMediarich-media/Subtype /RichMedia
ThreatType::NamedJavaScriptnamed-javascript目录中的 /Names/JavaScript 名称树
ThreatType::UriActionuri-action/S /URI 动作;当 allowUriActionstrue 时被抑制
ThreatType::Xfaxfa/XFA
ThreatType::UnparseableObjectunparseable-object任何解析失败的对象或目录
ThreatType::StripJavaScriptstrip-javascript可选加入的超集:/JS 键、/S /JavaScript/Subtype /JavaScript
ThreatType::StripEmbeddedFilesstrip-embedded-files可选加入:/Type /EmbeddedFile/Type /Filespec/EmbeddedFiles/EF
ThreatType::StripFormFieldsstrip-form-fields可选加入:/Subtype /Widget/FT 键或 /AcroForm
ThreatType::StripAnnotationsRichstrip-annotations-rich可选加入的子类型:MovieSoundFileAttachment3DRichMediaScreen
ThreatType::StripOcgNonDefaultstrip-ocg-non-default可选加入:带有 /Usage/Visibility 键的 /Type /OCG
ThreatType::StripDigitalSignaturesAtRebuildstrip-digital-signatures-at-rebuild可选加入:/Type /Sig/FT /Sig/DSS/VRI/ByteRange
ThreatType::Strip3dAndRichMediastrip-3d-and-rich-media可选加入的子类型:3DU3DPRCRMFRichMediaSoundMovie

CdrEngine::sanitize 执行六个有序阶段,且对敌意输入绝不抛出异常。

  1. 解析。 解析失败会返回一个 admitted 为 false、并带有解析错误拒绝原因的结果。此情况下清洗后的输出为空。
  2. 准入控制。 对象数量、聚合的解码流字节数、每流膨胀比以及页数会被对照策略限制进行检查。超限文档会被拒绝,而非清洗。拒绝与清洗被区分报告。
  3. 检测。 ThreatDetector::detect 扫描每个对象以及尾部目录以查找策略指定的威胁类型。无法解析的对象会被记录为 ThreatType::UnparseableObject 发现,而不是被跳过。
  4. 过滤。 携带发现的对象会被排入移除队列。文档目录永远不会作为整个对象被移除。目录层级的发现(OpenActionAdditionalActionsNamedJavaScript)改由键剥离来修复。
  5. 引用清除。 在序列化过程中,指向已移除对象的每个间接引用都会被替换为 null
  6. 重构。 CdrRebuilder::rebuild 生成一个单修订版 %PDF-2.0 文件,其中对象已重新编号,附带一张经典交叉引用表以及一个全新的尾部。安全的流字节以逐字节相同的方式复制。重构后的目录会丢弃 /OpenAction/AA/Names/AA 会从每个对象上丢弃。

返回的 CdrResult 暴露重构后的字节、被移除威胁的列表、两侧的字节大小、准入标志以及拒绝原因。如果源文件曾有一个可解析的 /Root 而重构输出丢失了它,引擎会拒绝该输出,而不是返回一个结构损坏的文件。这是一项失败即关闭(fail-closed)的保证:admitted 为 true 即意味着输出仍然携带一个文档目录引用。

增量更新永远不会存留:重构在每种策略下都精确地序列化恰好一个修订版,因此影子式的后期修订会因构造方式而被拍平。原有的数字签名无法在重构后保持有效,因为字节范围(byte range)不再与输出匹配。

架构红线。 CDR 是一个安全投影层,而非保全层。其输出不得用于法律证据保全、与原件的哈希比对或归档副本。

  • null 策略会解析为 CdrPolicy::standard()。以默认空 removeThreatTypes 构造的策略不会检测也不会移除任何内容。
  • allowUriActions 设为 true 会抑制 UriAction 移除,即使该枚举项存在于 removeThreatTypes 中也是如此。
  • flattenIncrementalUpdates 在本版本中是声明性的:重构在每种策略下都生成单个修订版,包括将该标志设为 falsepermissive()
  • 膨胀比检查将原始流长度为零视为一,因此从无到有膨胀的流仍然受到约束。当不保留任何解码形式时,原始流长度会计入聚合预算。
  • 页数准入检查是尽力而为的:目录或页面树读取失败本身不会拒绝文档。对象数量与解压预算始终强制执行。
  • 一个原始流长度与其整数 /Length 条目不一致的对象会在重构时被跳过(多态文件防御)。指向此类被跳过对象的引用会保留其源对象编号,且可能无法在输出中解析。sanitize() 会拒绝可检测的损坏结果(缺失 /Root),但直接驱动底层 CdrRebuilder::rebuild() 的调用方必须自行重新校验输出结构与引用完整性。
  • 当源尾部携带 /ID 时,重构后的尾部会携带一个全新生成的随机 /ID,而非原来的那个。其他尾部条目(包括 /Info)不会被沿用;重构后的尾部持有 /Size、可解析时的 /Root 以及重新生成的 /ID
  • 解码后的名称与键字节在重新生成时,会对分隔符、空白以及不可打印字节使用十六进制转义,因此敌意名称无法向输出中注入字典语法。
  • 位于已知名称取值集合之外的字典键之下的字符串值,会被保守地以字面字符串形式生成。
  • CdrPolicy::legacyThreatTypes() 会将任何未来的枚举项视为默认移除,除非它被登记为 Strip* 项,因此新的有损项无法悄然进入默认策略。
  • CDR 不是一个密码学模块。它唯一的随机性用途是重新生成的尾部 /ID。签名验证不在此处范围内;参见 签名深度参考
主张标准条款
调用一个 ECMAScript 动作会使 PDF 处理器执行其嵌入脚本。ISO 32000-2§12.6.4.17
JavaScript 名称树中的文档级脚本会在文档打开时全部执行。ISO 32000-2§12.6.4.17
目录名称字典可以持有一个由文档级脚本动作构成的 JavaScript 名称树。ISO 32000-2§7.7.4 (Table 32)
一个启动(launch)动作会启动一个应用程序,或打开或打印一个文档。ISO 32000-2§12.6.4.6
/AA 附加动作字典会扩展注释、页面、字段以及目录上的触发事件。ISO 32000-2§12.6.3
不可信文件的摄入必须约束进入文件的存在性、体量与内容。OWASP ASVS 5.0§5.2
系统应防止已上传文件的不当执行并检测危险内容。OWASP ASVS 5.0§5.3

所有条款均为释义;NextPDF 不复制规范性文本。NextPDF 不作任何认证主张。 CDR 在所配置策略下移除由 ThreatType 枚举的活动内容面;它是一项能力,而非一个经认证的清洗器。CDR 不是杀毒扫描器,也不检测恶意软件特征;它补充而非满足诸如 OWASP ASVS 5.4.3 杀毒扫描之类的控制项。一个已被解除武装的文件对某条给定摄入流水线是否可接受,仍然是操作方的风险决策。

  • 模块源码携带 @since 1.9.0;本参考记录的是随 nextpdf/enterprise 3.1.0 发布的接口面。
  • 一切都在你的主机上进程内运行。清洗期间不发生任何网络访问。
  • CdrPolicyDetectedThreatfinal readonly;要更改限制请构造一个新的策略实例。
  • CdrEngine 在内部构造其检测器与重构器。ThreatDetectorCdrRebuilder 仍可直接使用,以供那些提供自己 PdfReader 的分阶段流水线。
  • CdrRebuilder::rebuild$policy 参数目前是保留的;源码将其记录为为了调用点兼容性以及未来按策略的序列化调整而保留。
  • 输出是结构可复现的,而非按位可复现的:当源文件曾携带 /ID 时,重新生成的 /ID 在每次运行时都不同。
  • 结果类型 CdrResultsanitize() 的返回值)在上文已从行为上覆盖;其字段为 public readonly,并以 hadThreats()threatCount() 作为便捷方法。

本页仅记录外部可观测的行为以及受支持的公开 API 面。内部命名空间路径、辅助类、机制表、运行手册文件名以及工单前缀均不在范围内。