Security and operations
At a glance
Section titled “At a glance”This bridge sends your Hypertext Markup Language (HTML) across a network boundary to a browser engine. This page documents the controls that protect that boundary, using the source as the point of truth. When a control cites a standard, the citation is the one declared in the code docblock. This page restates the code’s assertion; it does not reconstruct normative wording.
Threat model
Section titled “Threat model”The package docblocks name the threats it defends against:
- XSS-to-PDF — Cross-site scripting (XSS) through hostile markup that runs during Portable Document Format (PDF) rendering.
- SSRF — Server-side request forgery (SSRF) caused by markup or a destination Uniform Resource Locator (URL) that sends a request to an internal address.
- Resource exhaustion — Oversized input or a decompression bomb.
- DNS rebinding — Domain Name System (DNS) rebinding, where a hostname passes validation and then resolves to a private address at connection time.
- On-path TLS interception — On-path Transport Layer Security (TLS) interception through a substituted certificate on the path to the Worker.
Each threat has a specific, testable control below.
Input controls (before the request leaves PHP)
Section titled “Input controls (before the request leaves PHP)”CloudflareSecurityPolicy::validate() runs before any request is
built:
| Control | Behavior | Limit source |
|---|---|---|
| Size cap | Rejects HTML larger than maxHtmlSize | CloudflareRendererConfig, default 5000000 bytes |
| Base64 decompression-bomb guard | Estimates the decoded size of every data:…;base64,… URI; rejects values at or above the ceiling | MAX_DATA_URI_BYTES = 13631488 |
| Meta-refresh ban | Rejects any <meta http-equiv="refresh">, case-insensitively | regex in CloudflareSecurityPolicy |
A violation raises RuntimeException with a message that names the
offending value and the limit. The meta-refresh ban exists because a
refresh directive can start navigation inside the page the Worker
renders — an SSRF vector that lives in content, not in the URL.
The HTML security policy from nextpdf/core
(HtmlSecurityPolicyInterface, default DefaultHtmlSecurityPolicy)
runs at the parse layer and complements the transport-layer checks
above. Retrieve it with getHtmlSecurityPolicy(). Inject a custom one
through the constructor.
Destination controls (SSRF and DNS rebinding)
Section titled “Destination controls (SSRF and DNS rebinding)”CloudflareSecurityPolicy::validateWorkerUrl():
- Rejects a URL that fails to parse or lacks a scheme/host
(
Invalid Worker URL). - Rejects any scheme other than HTTPS (
Worker URL must use HTTPS). - For an IP-literal host, rejects private or reserved ranges with
PHP’s
FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE. In practice, this rejects RFC 1918 private space, loopback, and RFC 3927 link-local addresses. Tests explicitly cover192.168.x,127.0.0.1, and169.254.xrejections. PHP’s filter extension decides range membership; this package does not pin that decision to a clause. RFC 1918 and RFC 3927 are named here descriptively as the well-known definitions of those ranges. - For a hostname, resolves all A and AAAA records with
dns_get_record()(notgethostbyname(), which returns only the first answer) and rejects the host if any resolved address is private or reserved.
The use of all-records resolution is deliberate. The class docblock documents it as a defense against a host that returns several records, where a single-record lookup might choose the public address while the later connection chooses a private one. This matches the OWASP SSRF Prevention Cheat Sheet: resolve both A and AAAA answers for the domain and apply the non-public-address check to the full result set.
validateWorkerUrl() returns the vetted IP set. Immediately before
sending, the renderer calls assertPinsStillValid(). That call
re-resolves the host and rejects a newly seen IP (Worker URL DNS answer changed since validation — possible DNS rebinding attack). This closes
the time-of-check / time-of-use window between validation and
connection.
Transport controls (PinnedCurlTransport)
Section titled “Transport controls (PinnedCurlTransport)”When a vetted IP set or a Subject Public Key Info (SPKI) pin set is
present and a PHP Standards Recommendation 17 (PSR-17)
ResponseFactory was supplied, the renderer uses
Transport\PinnedCurlTransport instead of the injected PHP Standards
Recommendation 18 (PSR-18) client. The transport enforces these
controls at the cURL handle layer:
- Pinned DNS —
CURLOPT_RESOLVEbinds the host:port to the vetted IP set, so libcurl does not perform its own lookup at connect time. This binding makes the userland DNS check apply to the actual connection; without it, libcurl could resolve a different address. - TLS public-key pinning —
CURLOPT_PINNEDPUBLICKEYis set from the combined pin set. This follows RFC 7469 §2.6: a pinned connection is accepted when the server-presented SPKI fingerprint set intersects the configured pin set, and a pin-validation failure is non-recoverable. Pin strings are normalized fromsha256/<base64>to cURL’ssha256//<base64>form; a malformed pin raisesInvalidSpkiPinException. - TLS verification on —
CURLOPT_SSL_VERIFYPEER => true,CURLOPT_SSL_VERIFYHOST => 2. - No automatic redirects —
CURLOPT_FOLLOWLOCATION => false,CURLOPT_MAXREDIRS => 0. A 3xx response is surfaced to the policy layer instead of being followed by libcurl to an unvetted host. The class docblock states that this is deliberate, so redirects are re-validated instead of silently followed. - Hard timeout —
CURLOPT_TIMEOUTis set fromrenderTimeout(default30seconds).
A cURL error or non-string body raises CloudflareRenderException with
the cURL error number and message.
Pinning operational guidance
Section titled “Pinning operational guidance”The configuration carries pinnedPublicKeys and a separate
backupPublicKeys. RFC 7469 §2.5 describes a backup pin as a
fingerprint for a secondary, not-yet-deployed key pair kept offline, and
treats it as the primary recovery path for inadvertent pin-validation
failure. Keep at least one backup pin so certificate rotation does not
brick the endpoint. The separate field lets you validate a rotation
independently. Operationally:
- Pin the SPKI of the leaf or of an intermediate whose rotation you control.
- Always configure a backup pin for the next certificate before rotating.
- An empty pin set disables pinning; use that only with a stable, known certificate chain. Pinning is opt-in by configuration.
Authentication and secret handling
Section titled “Authentication and secret handling”- The Worker request carries
Authorization: Bearer <apiToken>.apiTokenis#[SensitiveParameter], so stack traces redact it. The reachability probe sends the same bearer header on a Hypertext Transfer Protocol (HTTP)HEAD. - Cloudflare R2 access keys (
accessKeyId,secretAccessKey) are#[SensitiveParameter]and used only to derive the Amazon Web Services (AWS) Signature V4 signing key. ApiKeyValidatorcompares keys withhash_equals()(timing-safe) and supports Secure Hash Algorithm 256 (SHA-256) hashed-key storage viavalidateHashed().- Configuration objects are
final readonly— a secret set once cannot be mutated. - Source secrets from environment variables or a secrets manager. Never
commit them. The package follows the wider NextPDF security baseline:
PHPStan Level 10,
declare(strict_types=1)on every file, noeval()/exec(), GitHub Actions pinned to SHA.
What this package does not assert
Section titled “What this package does not assert”- It states no Cloudflare platform limit (Worker CPU time, memory, request body ceiling, or subrequest count). The only size and time limits this documentation states are the ones the package enforces itself, listed above and in /integrations/cloudflare/configuration/. For platform limits, consult Cloudflare’s official documentation and your Worker’s own implementation.
- It does not sign PDFs and makes no signature-conformance claim. When signatures are required, render here, then sign with the engine. NextPDF Pro provides PDF Advanced Electronic Signatures (PAdES) B-B signing only; long-term-validation profiles are an Enterprise capability and are out of scope for this bridge.
- It does not certify, guarantee, or render the pipeline “tamper-proof”. It implements only the specific, source-verifiable controls described on this page.
Operational runbook
Section titled “Operational runbook”| Symptom | First check |
|---|---|
Worker URL must use HTTPS | Check the configured workerUrl scheme. |
private or reserved IP | The Worker hostname’s DNS records; look for a record resolving into RFC 1918 / loopback / RFC 3927 space. |
DNS answer changed since validation | DNS instability or a rebinding attempt; re-resolve and inspect the full record set. |
cURL transport error | The network path, TLS chain, and — if pins are set — whether the served certificate’s SPKI is still in the pin set. |
| Renders fail right after a cert rotation | A pin set without a matching backup pin. Add the new SPKI as a backup before rotating. |
is not installed / no LocalRendererFactoryInterface | Fallback is enabled but no factory is wired, or nextpdf/artisan is absent. |
| Rate limit denials inconsistent across nodes | The in-memory limiter is per-process; front it with a shared store. |
Incident reporting
Section titled “Incident reporting”Report vulnerabilities through GitHub Security Advisories or the
security contact in the repository SECURITY.md. Do not file security
issues as public GitHub issues.
See also
Section titled “See also”- /integrations/cloudflare/overview/ — why this package is shaped around the boundary.
- /integrations/cloudflare/configuration/ — pin-set and limit fields.
- /integrations/cloudflare/troubleshooting/ — full failure-to-exception mapping.