Guard Pdf
PDF content-safety guard — refuses RCE-class PDF features without vendoring a parser. Operators bring their own PDF library (pdf-lib, pdfjs-dist, vendored mupdf) and feed structural metadata to the guard. KIND="metadata" — consumes ctx.metadata shape { bytes?, hasJavaScript?, hasOpenAction?, hasEmbeddedFiles?, hasLaunchAction?, isEncrypted?, pageCount?, embeddedFileCount?, polyglotDetected? }.
JavaScript exec refusal: /JS and /JavaScript annotations trigger RCE in vulnerable readers (the Adobe / Foxit / nitro CVE class). metadata.hasJavaScript === true is refused under every profile (javascriptPolicy: "reject" in strict / balanced / permissive). The framework refuses to negotiate on this — there is no audit-only path for executable JavaScript inside a PDF.
Embedded files refusal: /EmbeddedFile entries may smuggle executable payloads inside an otherwise-benign-looking PDF. strict refuses any embedded file (maxEmbeddedFileCount: 0); balanced audits up to 10; permissive audits up to 100.
OpenAction refusal: /OpenAction runs on document open. Standalone it's a navigation hint; paired with JavaScript or LaunchAction it's a drive-by trigger. strict refuses; balanced / permissive audit. JavaScript / LaunchAction are refused independently so the pairing can't slip through.
GoTo / Launch refusal: /Launch actions invoke an external program (the historical "open this .exe attached to the PDF" class). Refused under every profile (launchActionPolicy: "reject"). The framework keeps the exec surface closed.
Stream / object caps: maxPageCount (strict 500, balanced 5 000, permissive 50 000), maxBytes (strict 64 MiB, balanced 128 MiB, permissive 512 MiB), maxEmbeddedFileCount (strict 0, balanced 10, permissive 100). Operator-supplied — the operator's parser reports the structural counts; the guard refuses on excess.
Magic-byte check: %PDF- header (5 bytes 25 50 44 46 2D). Missing magic flagged under strict / balanced (the operator may be feeding non-PDF bytes through the wrong gate).
Polyglot rejection: when the operator's parser flags the buffer as polyglot (polyglotDetected: true), the guard refuses under every profile (polyglotPolicy: "reject").
Encrypted-PDF posture: many AV / sandbox tools can't scan encrypted documents. strict refuses; balanced audits; permissive allows.
Operator-feeds-metadata pattern: the gate trusts the metadata object the operator's parser reports. The framework's no-deps stance argues against shipping a vendored PDF parser; the operator's parser is the ground truth and the guard enforces the policy boundary.
Profiles strict / balanced / permissive and compliance postures hipaa / pci-dss / gdpr / soc2 overlay on the profile baseline.
b.guardPdf.validate(input, opts) #
{
profile: "strict"|"balanced"|"permissive",
compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
magicPolicy: "reject"|"audit"|"allow",
javascriptPolicy: "reject"|"audit"|"allow", // strict refused — RCE class
openActionPolicy: "reject"|"audit"|"allow",
launchActionPolicy: "reject"|"audit"|"allow", // strict refused — RCE class
embeddedFilePolicy: "reject"|"audit"|"allow",
encryptedPolicy: "reject"|"audit"|"allow",
polyglotPolicy: "reject"|"audit"|"allow",
pageCountPolicy: "reject"|"audit"|"allow",
embeddedFileCountPolicy: "reject"|"audit"|"allow",
maxPageCount: number, // strict 500, balanced 5000, permissive 50000
maxEmbeddedFileCount: number, // strict 0, balanced 10, permissive 100
maxBytes: number, // strict 64 MiB, balanced 128 MiB, permissive 512 MiB
}
Inspect a PDF metadata bag { bytes?, hasJavaScript?, hasOpenAction?, hasLaunchAction?, hasEmbeddedFiles?, isEncrypted?, pageCount?, embeddedFileCount?, polyglotDetected? } and return { ok, issues }. Detected: magic-missing (no %PDF- header), polyglot (operator- flagged), javascript-action (RCE class — universally refused), launch-action (universally refused), open-action (drive-by class), embedded-file / embedded-file-count, encrypted, page-count, pdf-cap. Pure inspection — never mutates input or throws on hostile metadata.
var rv = b.guardPdf.validate({
bytes: Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x37]),
hasJavaScript: true, pageCount: 1,
}, { profile: "strict" });
rv.ok; // → false
rv.issues[0].kind; // → "javascript-action"
rv.issues[0].severity; // → "critical"
// LaunchAction — universally refused.
var launch = b.guardPdf.validate({
bytes: Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x37]),
hasLaunchAction: true,
}, { profile: "permissive" });
launch.issues.some(function (i) { return i.kind === "launch-action"; });
// → true
b.guardPdf.sanitize(input, opts) #
{
profile: "strict"|"balanced"|"permissive",
compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
}
Disarm-by-refusal. PDF active content (/JavaScript, /Launch, /OpenAction, embedded files) and encryption live in a cross-referenced object graph; excising them safely needs a vendored PDF parser, which the framework does not ship (a parser per format is a supply-chain hop, and a fragile in-house excision on a security primitive is worse than an honest refusal). So sanitize forces every active-content / exfil / encryption policy to reject and re-throws GuardPdfError on any finding — it never hands back a PDF that still carries JavaScript, a launch/open action, embedded files, or encryption. A PDF with none of these passes through unchanged (genuinely nothing to strip). Operators needing a repaired file run a vendored disarm tool (e.g. qpdf --decrypt plus removing /OpenAction / /Names / /JavaScript).
try {
b.guardPdf.sanitize({
bytes: Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2D]),
hasJavaScript: true,
}, { profile: "strict" });
} catch (e) {
e.code; // → "pdf.javascript-action"
}
b.guardPdf.gate(opts) #
{
profile: "strict"|"balanced"|"permissive",
compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
name: string,
...: any validate opt
}
Build a b.gateContract gate suitable for b.fileUpload({ contentSafety: { "application/pdf": gate } }) or b.staticServe. Operators pass ctx.metadata (the parser's structural report) plus the original bytes. Action chain: serve (no issues) → audit-only (warn-only) → refuse (any critical / high). The gate does not rewrite bytes; b.guardPdf.sanitize(bag) is disarm-by-refusal (it refuses any PDF still carrying active content / embedded files / encryption rather than silently passing it through).
var pdfGate = b.guardPdf.gate({ profile: "strict" });
var verdict = await pdfGate.check({
metadata: {
bytes: Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x37]),
hasJavaScript: true, pageCount: 1,
},
});
verdict.action; // → "refuse"
verdict.issues[0].kind; // → "javascript-action"
b.guardPdf.inspectMagic(bytes) #
Return true when bytes starts with the PDF magic header (%PDF-, the 5 bytes 25 50 44 46 2D); false otherwise. Pure inspection — never mutates input or throws.
var pdfBytes = Buffer.from([0x25, 0x50, 0x44, 0x46, 0x2D, 0x31, 0x2E, 0x37]);
b.guardPdf.inspectMagic(pdfBytes); // → true
b.guardPdf.inspectMagic(Buffer.from([0x00, 0x01, 0x02]));
// → false
b.guardPdf.compliancePosture(name) #
Look up a compliance-posture overlay by name (one of "hipaa" / "pci-dss" / "gdpr" / "soc2"). Returns a fresh clone of the posture overlay so the caller may mutate it freely without disturbing the shared table. Throws GuardPdfError with code "pdf.bad-posture" when the name is not one this guard maps. Wired by gateContract.defineGuard through gateContract.lookupCompliancePosture, so the clone semantics and error code are identical across every guard in the family.
var posture = b.guardPdf.compliancePosture("hipaa");
posture; // → overlay clone (mutable)
try {
b.guardPdf.compliancePosture("not-a-regime");
} catch (e) {
e.code; // → "pdf.bad-posture"
}
b.guardPdf.buildProfile(opts) #
{
extends: string|string[], // base profile name(s) to compose
...: any guard key, // inline override of resolved keys
}
Compose a derived profile from one or more named bases plus inline overrides, resolving names through this guard's own PROFILES table. opts.extends is a base profile name ("strict" / "balanced" / "permissive") or an array of names — later entries shadow earlier ones, and inline opts keys win last. Wired by gateContract.defineGuard through gateContract.makeProfileBuilder, so operator-defined profiles stay traceable to a baseline instead of a hand-typed dictionary.
var custom = b.guardPdf.buildProfile({ extends: "strict" });
custom; // → composed profile object
b.guardPdf.loadRulePack(pack) #
Register an operator-supplied rule pack with this guard's rule-pack registry. The pack is identified by pack.id (a non-empty string) and stored for later dispatch by gates that opt in via opts.rulePackId. Returns the pack unchanged on success; throws GuardPdfError with code "pdf.bad-opt" when pack is missing or pack.id is not a non-empty string. Wired by gateContract.defineGuard through gateContract.makeRulePackLoader, so storage shape and validation are identical across the family.
var pack = b.guardPdf.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id; // → "tenant-policy"
b.guardPdf.resolveOpts(opts?) #
{
profile: string, // one of PROFILES; default this guard's default
compliancePosture: string, // overlay one of hipaa/pci-dss/gdpr/soc2
}
Resolve caller opts against this guard's PROFILES + compliance-posture overlays into the fully-defaulted option set the guard runs on — the same resolution validate / sanitize / gate apply internally. Wired by gateContract.defineGuard from the guard's binding config (profiles / postures / defaults / error class), so a guard's bespoke gate calls resolveOpts instead of re-declaring the per-guard resolver wrapper. Throws GuardPdfError with code "pdf.bad-opt" / "pdf.bad-posture" on an unknown profile or posture name.
var resolved = b.guardPdf.resolveOpts({ profile: "strict" });
resolved.profile; // → "strict"
Last updated 2026-08-08T16:39:49.652Z by seeder.