Security posture

How DocuShell handles untrusted PDFs, untrusted URLs, and high-volume API traffic

DocuShell is explicitly designed to safely process untrusted public inputs while maintaining zero shell-injection risks, zero server-side request forgery vulnerabilities, and zero unsafe file-execution vectors. This page is the public, evergreen version of the threat model and architectural controls used across the parse, compress, webpage-to-PDF, and markdown-to-PDF lanes.

Looking for vulnerability reporting, safe harbor, or our incident contact? See the Security Policy.

Architecture in one paragraph

DocuShell is a zero-trust monorepo of stateless containerized workers coordinated asynchronously by a Redis-backed BullMQ queue. Public traffic enters through a hardened API gateway at apps/api that owns authentication, rate limiting, idempotency, and request validation; the gateway forwards work over an internal trust boundary to single-responsibility worker services (parse-pdf, compress-pdf, webpage-to-pdf, markdown-to-pdf). Workers never accept public traffic directly.

  • Every public submit lane runs behind a hardened API gateway that fails closed when idempotency, rate-limit, or validation infrastructure is degraded.
  • Each microservice owns a single bounded responsibility (parse, compress, render, webpage) and is isolated by container, queue, and trust boundary.
  • Uploaded and generated documents live in ephemeral processing storage only and are removed after stream completion or within the one-hour cleanup window.

Input validation

Magic-byte validation before transport

PDF payloads are validated against the %PDF magic-byte signature in the client buffer reader before they cross the network. Files that do not declare a PDF magic byte are rejected before storage allocation.

services/compress-pdf/src/validation.js, packages/api-core/* input gates.

Strict Zod schemas without coercion

External Node endpoints validate request bodies with explicit Zod schemas. String inputs supplied for integer fields throw HTTP 400 immediately rather than being silently coerced. This eliminates an entire class of type-confusion exploits at the edge.

Encrypted and corrupt-file rejection

Compress requests invoke QPDF integrity routines internally to isolate encrypted blobs and locked keys. Damaged or password-protected files are rejected with a typed 400 error before backend queue payload limits are allocated.

Metadata scrubbing by default

Output PDFs are sanitized to remove common document info fields — Title, Author, Subject, Keywords, Creator, Producer — before delivery. The optional sanitize flag additionally masks email addresses, URLs, and phone numbers in extracted text output.

Network and SSRF protections

Expanded SSRF blocking on every URL fetch

Shared URL validation rejects private, localhost, AWS metadata, CGNAT, documentation, benchmarking, multicast, and future-use address ranges before any Chromium worker or remote-fetch worker can connect. Validation runs on both dns.resolve4() and dns.resolve6() bindings.

services/webpage-to-pdf/src/lib/validation.js, shared URL validators in apps/api.

Rate limit and request-size shields

Webpage-to-PDF and Compress-PDF endpoints natively enforce Express rate-limit shields tracking 20 requests/min alongside 1 MB object JSON maximums, stopping unbounded request flows before they reach worker processes. The default API-key throttle is 10 RPM unless operations sets API_KEY_RPM.

Client-readable backpressure

Plan concurrency caps include Retry-After headers, so SDKs and customer workers can back off gracefully instead of hammering the gateway during capacity events.

API gateway hardening

The public submit lanes (Compress, Parse, Resume Parse, Webpage-to-PDF, PDF-to-Word, Markdown-to-PDF, and Parse Batch) share a single hardened gateway in apps/api/src/lib/apiSubmitSafety.ts. Every behavior below is observable from the response headers and status codes.

Idempotency-Key safety in-flight

If a retry arrives while the original request is still in flight, the gateway returns 425 idempotency_in_progress with Retry-After instead of replaying a provisional queued response. Reusing the same key with a different payload returns 409 idempotency_key_reused.

Idempotency storage fails closed

If the idempotency Redis backend is unavailable, public submit routes return 503 server_busy with Retry-After rather than accepting duplicate expensive work and double-charging credits.

Trusted internal identity for backend calls

Gateway-to-service calls use a stable User-Agent: DocuShell-API/1.0 plus a trusted API-auth-derived client IP, never a caller-controlled browser or bot fingerprint. Downstream services can therefore rely on the forwarded identity for audit logging.

Strict secret configuration

Legacy INTERNAL_API_SECRETS entries are semicolon-separated only, cached at process start, reloadable with SIGHUP, and comma-separated values are treated as invalid configuration. Plan limits are enforced before the job is queued, not after the worker starts.

Webhook integrity

Signed payloads with rotation

Every webhook delivery is signed with HMAC-SHA256 over a timestamped payload (t=<unix_ts>.<raw_body>). During a key rotation window the request includes both the current and the previous v1 signature so receivers can verify either secret without downtime.

Replay nonce and timestamp window

Webhook entitlement claims are timestamped HMACs with a short acceptance window. Replay nonces are stored in HA Redis with TTL via WEBHOOK_NONCE_REDIS_URL or Sentinel/REDIS_URL fallback, so material clock drift or Redis outage rejects otherwise valid webhook traffic.

NTP-synchronized server clocks

Production deployments require NTP-synchronized clocks between API server and worker host. Drift outside the acceptance window is treated as an authenticity failure rather than processed at risk.

Webhooks are available on every paid plan (Starter and above). See the webhook integration guide for delivery format, headers, and verification examples.

Document lifecycle and storage

Ephemeral processing storage only

Heavy jobs run on isolated cloud workers. Uploaded and generated files live in temporary processing storage solely for job completion and download delivery — never in long-term storage.

Atomic download cleanup

Generated files are deleted after download completion, including interrupted streams. Any remaining temporary artifacts are swept within one hour by the cleanup worker to minimize the exposure window.

Requester-scoped artifact links

Completed job downloads are streamed only to the authenticated requester. profile_result metadata is redacted once output_expires_at is reached, so audit metadata survives the valid artifact window without leaking past it.

Threat model summary

The table below maps the threat vectors we explicitly design against to the specific control that mitigates each one.

Threat vectorMitigation
Shell injection via user filenames or contentGhostscript and QPDF are invoked through secure child_process handlers with whitelisted argument arrays; no user input is ever passed to a shell.
SSRF via user-supplied URLs (webpage-to-PDF, remote fetch)Pre-resolution DNS validation across IPv4/IPv6 blocks private, localhost, metadata, CGNAT, doc, benchmarking, multicast, and future-use ranges. No worker can connect until validation passes.
Duplicate billing from client retriesIdempotency-Key with in-flight detection (425 idempotency_in_progress), payload mismatch detection (409 idempotency_key_reused), and fail-closed Redis backend (503 server_busy).
Webhook replay attacksTimestamp window + HA Redis replay-nonce storage with TTL; both rejection conditions are explicit before processing.
Webhook secret leakage during rotationDual-signature support (current + previous) during a defined rotation window; rotated secrets do not invalidate in-flight deliveries.
Malicious uploads (non-PDF disguised as PDF)Client-side magic-byte check before upload + server-side QPDF integrity routines before queue allocation.
Resource exhaustion / DoSExpress rate-limit shields (20 RPM defaults), 1 MB JSON cap, plan concurrency caps with Retry-After, idempotency storage fail-closed.

Reporting a vulnerability

  • Email security reports to [email protected] with affected URL or endpoint, reproduction steps, expected impact, and safe proof-of-concept details.
  • Do not include live user documents, credentials, secrets, or personal data in your report — use synthetic test files.
  • We do not currently operate a paid bug bounty program. Good-faith reports are reviewed promptly and protected under the safe-harbor terms in our Security Policy.

Full safe-harbor terms, in-scope and out-of-scope reports, and testing rules are documented in the Security Policy.

Where to go next

  • Webhook integration guide — signed payload format, verification, and rotation behavior.
  • API hub — public endpoint reference, plan limits, and rate-limit behavior.
  • Privacy policy — analytics, advertising, retention, and data-subject requests.