Please do NOT open a public issue for security vulnerabilities.
To report a security vulnerability, please use GitHub's private vulnerability reporting.
Alternatively, contact us at: security@pdfnative.dev
We will acknowledge receipt within 48 hours and aim to provide a fix within 7 days for critical issues.
| Version | Supported |
|---|---|
| 1.4.x | ✅ |
| 1.3.x | ✅ |
| 1.2.x | ✅ |
| < 1.2 | ❌ |
pdfnative-cli is a thin dispatch layer over the pdfnative library. It introduces zero additional runtime dependencies. All PDF cryptographic operations are performed inside pdfnative — see the pdfnative security policy for the full cryptographic implementation notes (RSA, ECDSA, AES).
The CLI exposes 21 commands (run pdfnative --help or pdfnative schema manifest
for the authoritative list). The sign, verify, ltv and doc-timestamp
commands handle key material, certificate chains and trusted-timestamp tokens;
security invariants for each are described below.
The agent-native contract is a pure local presentation/validation layer and adds no network surface:
--jsononly changes how diagnostics are formatted on stderr (a machine-readable envelope). It never opens sockets, never alters what is written to stdout, and never relaxes any security check.--dry-runvalidates inputs and short-circuits before producing or writing output — and never performs network I/O, even when a network flag (--timestamp,--url,--online) is present. Forsignit stops after credentials are parsed and the PDF is prepared, before any signature value is computed — and still never logs key material.- Stable
E_*error codes carry only a failure class and a redacted message; internal byte offsets, parser state, and key bytes are never exposed (thesignfailure message stays the fixedFailed to sign PDF.). TSA / OCSP / CRL response bodies are never echoed into CLI output (E_NETWORKmessages are generic). - The CLI remains offline by default in every mode; network I/O happens only behind the explicit opt-in flags listed under Network Access below, and only through the SSRF guard.
- Private keys are loaded from the
PDFNATIVE_SIGN_KEYenvironment variable (PEM string) or from a file via--key. Environment variable takes precedence over file paths. - Keys are never written to disk, logged, or included in error messages. All
signPdfBytesfailures are replaced with the fixed stringFailed to sign PDF.(error codeE_SIGN). - PEM strings are consumed directly from memory and not persisted beyond the signing call.
- Native constant-time crypto by default. CMS signing routes through Node's
node:crypto(createNativeCryptoProvider) for side-channel-resistant RSA/ECDSA.--pure-cryptoselects pdfnative's portable pure-JS bignum path; both keep key material in memory only. - Recommendation for high-frequency pipelines: use
PDFNATIVE_SIGN_KEYwith a secrets manager (AWS Secrets Manager, Vault, GitHub Actions secrets) rather than a file on disk.
- All file path arguments (
--input,--output,--output-dir,--key,--cert,--cert-chain,--layout,--attachment,--watermark-image,--outline,--annotations,--trust,--data,--from-json,--manifest, the positional source paths ofmergeandcompare, and every path-carrying value inside abatch --manifestfile) are validated against path traversal (../) sequences before any filesystem access. - JSON input size is capped at 50 MB before
JSON.parseto prevent memory exhaustion (this also covers theannotate --annotationsspec,govern verify-issuedrafts,ltv --datafiles andbatch --manifestfiles; manifests are additionally capped at 1 000 tasks). - A manifest has the filesystem access of the user who invokes
batch— the same trust level as flags typed on the command line. Only network access is additionally gated: any network-reaching flag inside a manifest requires--allow-networkon the invocation itself, so a manifest obtained from elsewhere can never open a socket on its own. - The global
--max-inflate-size <bytes>flag caps the decompressed size of any single PDF stream while parsing untrusted input (anti zip-bomb; engine default 100 MiB). merge/split/extractenforce an optional--max-output-sizecap and bound the number of source PDFs;extract/annotatebounds-check every page reference against the document before writing.annotatere-keys only the annotation fields pdfnative's builders understand — the raw JSON is never spread into the emitted dictionary, so unknown keys cannot be injected.inspectJSON output sanitizes all values — no raw binary blobs are emitted in default mode.
- No
eval(),Function(), or dynamic code execution (batch --manifestdispatches only to a fixed whitelist of CLI command modules — never to arbitrary code). - Offline by default — no command opens a socket unless you pass one of the
explicit opt-in flags listed below. The
governcommand (AI-governance / HITL) is fully offline: it never contacts GitHub or the network, andgovern verify-issueis a pure local validator. See Network Access below. - NPM provenance — signed builds via GitHub Actions OIDC.
The CLI is offline by default. Exactly four flags can cause a network request, each naming the operation it enables:
| Opt-in | Command | What is fetched |
|---|---|---|
--revocation online |
verify |
OCSP (AIA) + CRL (CDP) revocation data |
--timestamp <url> |
sign |
An RFC 3161 timestamp token from the named TSA |
--url <url> |
doc-timestamp |
An RFC 3161 token for the /DocTimeStamp revision |
--online |
ltv collect / ltv add |
OCSP + CRL validation data to archive in /DSS |
Inside a batch --manifest pipeline these flags are additionally refused unless the
batch invocation itself carries --allow-network. ltv embed is network-free by
design (air-gapped embedding of pre-collected data), and --dry-run never opens a
socket in any command.
Every request passes through the same SSRF guard (src/utils/fetch-guard.ts) that
enforces:
- an http/https-only scheme allow-list;
- DNS resolution followed by address vetting — requests to private (RFC 1918),
loopback, link-local (incl. the
169.254.169.254cloud-metadata address), unique-local, CGNAT (100.64.0.0/10) and multicast ranges are refused, for both IPv4 and IPv6 (including IPv4-mapped IPv6); - no redirect following (a 3xx is refused rather than followed, so a redirect to an internal host cannot bypass the address check);
- a 10 s timeout and a 5 MiB response cap;
- no cookies, no auth, no connection reuse.
Embedded revocation data (OCSP responses / CRLs in the PDF /DSS) and RFC 3161
timestamp tokens are parsed offline with no network access. All such signatures
(CRL tbsCertList, OCSP tbsResponseData, TSA SignerInfo) are cryptographically
verified against the issuing certificate; unverifiable data yields an unknown
status, never a good one.
The verify command verifies, with no network access by default:
- Byte-range integrity — the signer's declared digest (SHA-256, SHA-384 or
SHA-512) of the covered bytes vs the CMS
messageDigest. - CMS signature value — RSA-PKCS#1 v1.5 with SHA-256/384/512, and ECDSA-SHA256
(P-256), over the re-encoded
signedAttrs. ECDSA with SHA-384/512 is detected and labelled but never reported valid (verification is P-256 + SHA-256 only). - Certificate chain & trust — chain construction and evaluation against
--trustroots (or self-signed acceptance when no roots are supplied). - RFC 3161 timestamp (PAdES-T) — the TSA SignerInfo signature, the TSTInfo
eContent digest, and the
messageImprintbinding to the document signature are validated, and the TSA chain is built/trust-evaluated. Reported astimestampValid. /DocTimeStamprevisions (PAdES B-LTA) — each document timestamp's token is parsed, itsmessageImprintis checked against the covered byte range, and the TSA token signature is verified; reported withisDocTimestamp: true.- OCSP (RFC 6960) + CRL (RFC 5280) revocation — embedded
/DSSdata (offline) and, with--revocation online, AIA/CDP fetches via the SSRF-guarded client.
Sign-side LTV is available since v1.4.0: sign --timestamp (PAdES B-T),
ltv collect|embed|add (B-LT, /DSS + /VRI) and doc-timestamp (B-LTA). The
engine (pdfnative 1.7.0) verifies every TSA token before embedding it and never
opens a socket itself — the CLI injects the SSRF-guarded transport.
Out of scope (do not rely on for legal / regulatory non-repudiation):
- Full PAdES-B-LTA archival validation — evaluation of a document-timestamp chain over time (renewal policy, algorithm rollover assessment) is not performed; each timestamp is validated individually.
- TSA certificate revocation — the revocation status of the TSA's own certificate is not checked.
All signature-relevant hashing and verification uses SHA-256 or stronger (see the scope above). SHA-1 appears in two deliberate places:
- Verification of legacy timestamp imprints — an existing RFC 3161 token whose
messageImprintwas computed with SHA-1 is still checked (the digest named by the token's ownhashAlgorithmis used for the comparison). This affects verification of third-party documents only; the CLI always requests SHA-256+ imprints when it timestamps (--timestamp-digest/--digest, default sha256), and the token's TSA signature itself must verify with SHA-256+. - The OCSP
CertIDbuilt bybuildOcspRequestand matched inocspCertIdMatches(src/utils/revocation.ts). (SHA-1 of a signature's/Contentsis also used as the — non-cryptographic —/VRIdictionary key, as required by ISO 32000-2.)
The CertID usage is intentional and safe:
- RFC 6960 §B.1 defines SHA-1 as the default
CertIDhash algorithm, and it is the only one reliably indexed by deployed OCSP responders; using SHA-256 would make most responders answerunknown. - The
CertIDhash is a non-security identifier computed over the issuer's public subject DN and public key — it is not an integrity or signature primitive. OCSP trust is established solely by the responder's digital signature, which is verified independently with SHA-256/ECDSA. - NIST SP 800-131A explicitly permits SHA-1 for such non-digital-signature applications.
Static analysers (e.g. CodeQL js/weak-cryptographic-algorithm) may flag this line
because certificate-derived bytes are treated as "sensitive data". This is a
reviewed false positive: the data is public and the hash is not used for any
security decision. The call sites are annotated in source, and the alert is dismissed
as "Won't fix" in code scanning with this rationale.
We follow coordinated disclosure. We ask that you:
- Report vulnerabilities privately (see above).
- Allow us reasonable time to fix and release a patch before public disclosure.
- Avoid testing against systems you do not own.