Artisan security and operations
At a glance
Section titled “At a glance”The bridge renders HTML that may be untrusted in Chrome, behind two independent network barriers and a strict content policy. Chrome’s operating-system sandbox is a separate, optional control with explicit limits. This page documents the boundary. It does not claim the boundary is absolute.
Conceptual overview
Section titled “Conceptual overview”A render is server-side request execution: your application hands HTML to a browser engine that can fetch resources by default. When untrusted input drives an outbound fetch, the risk is server-side request forgery (SSRF): Common Weakness Enumeration (CWE) entry CWE-918 defines it as a server retrieving the contents of a supplied URL without enough assurance that the request reaches the expected destination. SSRF (CWE-918) is a CWE Top 25 weakness. The Open Worldwide Application Security Project (OWASP) Application Security Verification Standard (ASVS) requires you to control outbound requests from server components instead of leaving them implicit. The OWASP SSRF Prevention Cheat Sheet treats a network-layer deny of calls to arbitrary destinations as the strong control. The deny-by-default network posture below is the bridge’s response to that requirement. National Institute of Standards and Technology (NIST) Special Publication (SP) 800-53 SC-7 describes the same deny-all-permit-by-exception boundary principle that the bridge applies at the transport layer.
Data residency and PII mitigations
Section titled “Data residency and PII mitigations”HTML passed to the bridge is processed entirely in-process and inside the local Chrome instance. The bridge makes no outbound network call of its own and blocks Chrome from making any (see the network model below), so input content does not leave the host through the renderer. Personally identifiable information (PII) in the input is rendered into the Portable Document Format (PDF) output you produce, so treat the output with the same residency controls as the input. The bridge does not persist input or output to disk; persistence is the caller’s responsibility.
Safe telemetry and log scrubbing
Section titled “Safe telemetry and log scrubbing”ChromeHtmlRenderer and BrowserPool accept an optional PHP Standard Recommendation (PSR)-3 LoggerInterface. The bridge logs only operational metadata: input byte length, target width and height, output byte length, measured content height, browser launch with the configured binary path, restart notices with a render count, and close events. It does not log HTML content, rendered bytes, or extracted text. This aligns with NIST SP 800-92 guidance to log operational events while keeping sensitive payloads out of logs. The binary path is logged. Treat it as non-sensitive deployment metadata. Tests assert the log call shapes in tests/Unit/Artisan/ChromeHtmlRendererTest.php::renderLogsDebugWithSizeWidthHeightAndPdfSize and tests/Unit/Artisan/BrowserPoolTest.php::getBrowserLogsInfoOnLaunchWithBinaryPath.
Network isolation model (defense in depth)
Section titled “Network isolation model (defense in depth)”The bridge applies two independent barriers so that one bypass does not expose the host:
-
Content-Security-Policy. Every render is wrapped by
ChromeSecurityPolicy::wrapHtml()in a document that carries:default-src 'none'; style-src 'unsafe-inline'; img-src data:;base-uri 'none'; form-action 'none'; frame-ancestors 'none';navigate-to 'none';Content Security Policy (CSP) directive
default-src 'none'denies all resource origins.img-src data:allows only inline images.navigate-to 'none'blocks client-side navigation.style-src 'unsafe-inline'is the only relaxation required for ChromeprintToPDFto apply inline styles. Verified insrc/Artisan/ChromeSecurityPolicy.phpand asserted byChromeSecurityPolicyTest::wrapHtmlIncludesNavigationCspDirectives. -
Chrome DevTools Protocol (CDP) transport block. Before content loads,
ChromeHtmlRendererissuesNetwork.enableand thenNetwork.setBlockedURLswith the pattern['*']. This blocks every subresource URL at the Chrome DevTools Protocol transport layer, independent of CSP. Verified insrc/Artisan/ChromeHtmlRenderer::blockAllNetworkRequests()and asserted byChromeHtmlRendererTest::renderAutoFitsHeightAndBlocksNetworkRequests(which checks the exact CDP method order and the['urls' => ['*']]parameter). This is the network-layer block OWASP SSRF guidance recommends as the strongest control, and it is a transport-level deny-all consistent with NIST SP 800-53 SC-7.
The result: a remote <img>, stylesheet, font, script, or iframe URL in the input does not load. The bridge does not implement a domain allowlist or private-IP filter because it does not need one: it permits no outbound subresource fetch at all.
Drift note: the
nextpdf/coredocblock onwriteHtmlChrome()says Chrome “will fetch external resources” and advises configuring a policy to “block private IP ranges and limit allowed domains.” That describes a configurable allowlist model. The shipped ArtisanChromeSecurityPolicydoes not expose an allowlist; it blocks all subresource requests unconditionally. The code, not the core docblock, is authoritative. This drift is recorded for the core docs team.
Input validation (pre-Chrome)
Section titled “Input validation (pre-Chrome)”ChromeSecurityPolicy::validate() runs before the bridge contacts Chrome and rejects:
| Check | Limit | Rationale |
|---|---|---|
| HTML size | > maxHtmlSize (default 5 MB) | Resource-exhaustion bound (CWE Top 25 uncontrolled resource consumption) |
| Base64 data URI | capture group >= 13_000_000 bytes | Decompression-bomb bound |
<meta http-equiv="refresh"> | any (case-insensitive, single/double quote) | Blocks client-side redirects to internal endpoints, an SSRF navigation vector |
Meta-refresh blocking is explicit SSRF hardening. Without it, attacker-controlled HTML could redirect Chrome to a cloud metadata endpoint before printToPDF. Boundary behavior is asserted across ChromeSecurityPolicyTest (validateThrowsOnOversizedHtml, validateRejectsMetaRefreshRedirect, validateRejectsMetaRefreshCaseInsensitive, validateRejectsMetaRefreshWithSingleQuotes, validateRejectsOversizedBase64DataUri, validateRejectsBase64DataUriAtExactThreshold).
Additionally, ChromeSecurityPolicy::wrapHtml() strips </style> from defaultCss before injection to prevent a style-block break-out into script context (asserted by ChromeSecurityPolicyTest::wrapHtmlStripsStyleClosingTagsFromDefaultCss).
The Chrome sandbox boundary — stated explicitly
Section titled “The Chrome sandbox boundary — stated explicitly”Chrome’s operating-system sandbox is a separate control from the network barriers above, and the bridge does not guarantee it.
- By default,
noSandboxisfalse, so Chrome launches with its own sandbox enabled. The bridge does not implement that sandbox; it relies on the Chrome binary’s sandbox, which depends on host kernel support. - Setting
noSandbox: truelaunches Chrome with--no-sandbox. This removes the Chrome process-isolation sandbox. It is provided for containers where the sandbox cannot initialize. It is a real reduction in isolation: a renderer compromise is no longer contained by Chrome’s sandbox. - The bridge’s network barriers (CSP + CDP block) remain in force whether or not the sandbox is enabled, but they are not a substitute for process isolation. OWASP ASVS least-privilege guidance applies: run Chrome as a non-root user, in a constrained container, with
noSandboxonly where unavoidable, and treat a--no-sandboxdeployment as a higher-trust requirement on the input.
This documentation does not claim the bridge is “secure by default” or “tamper-proof”. It also does not claim that disabling the sandbox is safe. It states the controls that exist and where they stop. Provisioning a sandbox-capable container is covered on the /integrations/artisan/chrome-renderer-setup/ page.
Failure modes
Section titled “Failure modes”These failure modes are enumerated from src/Artisan/Exception/ and the render/transport code:
| Condition | Surfaced as | Source |
|---|---|---|
chrome-php/chrome library absent | ChromeNotAvailableException (with install command) | BrowserPool::getBrowser() |
HTML exceeds maxHtmlSize | RuntimeException (“exceeds maximum allowed size”) | ChromeSecurityPolicy::validate() |
| Oversized base64 data URI | RuntimeException (“oversized base64 data URI”) | ChromeSecurityPolicy::validate() |
| Forbidden meta-refresh | RuntimeException (“forbidden meta refresh redirect”) | ChromeSecurityPolicy::validate() |
| Chrome launch / timeout / crash | ChromeRenderException (wrapping the cause) | ChromeHtmlRenderer::render() |
| Chrome returned empty PDF | ChromeRenderException (“returned empty data”) | ChromeHtmlRenderer::render() |
| Page has no content stream | PdfParseException | PageImporter::import() |
If ChromeRenderException is raised inside the render, it is re-thrown unchanged. Any other Throwable is wrapped as ChromeRenderException, preserving the previous exception (asserted by ChromeHtmlRendererTest::renderRethrowsChromeRenderExceptionWithoutWrapping and ::renderWrapsUnexpectedThrowablesWithChromeRenderException). The Chrome page is always closed in a finally block, even on failure.
Resource limits
Section titled “Resource limits”- Input size:
maxHtmlSize(default 5 MB) and the 13 MB base64 data-URI cap. - Time:
renderTimeoutseconds bounds both content load and CDP sync calls. CDP control commands use a fixed 5-second timeout. - Process:
BrowserPoolrestarts Chrome every 100 renders to bound memory growth and closes the process onclose()/ destruction.
These are bounds, not quotas. For any path exposed to untrusted input, still use a host-level resource limit (cgroup, ulimit, request budget), consistent with the CWE Top 25 resource-consumption guidance.
Observability hooks
Section titled “Observability hooks”Inject a PSR-3 logger to capture render start (size, width, height), render complete (output size, content height), browser launch (binary path), browser restart (render count), and browser close (render count). These are the only events emitted, and they carry no payload content. Use them for latency service-level objectives (SLOs) and restart-rate alerting.
Conformance
Section titled “Conformance”| Claim | Reference | clause_id | reference_id |
|---|---|---|---|
| Outbound requests from server components must be controlled | OWASP ASVS 5.0 | § (SSRF/outbound control) | |
| SSRF = server retrieves a supplied URL without validating the destination | CWE Top 25 2025 (CWE-918) | cwe_top25_2025#x28.x2.p2 | |
| SSRF (CWE-918) is a CWE Top 25 weakness | CWE Top 25 2025 | cwe_top25_2025#x1.p73 | |
| Uncontrolled resource consumption is a CWE Top 25 weakness | CWE Top 25 2025 (CWE-400) | cwe_top25_2025#x19.x2.p2 | |
| Deny-by-default boundary protection (permit by exception) | NIST SP 800-53 Rev 5 SC-7 | SC-7 | |
| Network-layer deny of calls to arbitrary destinations is the strong SSRF control | OWASP Cheat Sheet Series (SSRF Prevention §Network layer) | owasp_cheatsheet_series#x132.x2 | |
| Protect URL-fetching components against SSRF | OWASP Cheat Sheet Series | § (SSRF prevention, URL-fetch tools) | |
| Isolate untrusted-content rendering, least privilege | OWASP ASVS 5.0 | § (sandbox / least privilege) | |
| Log operational events; keep payloads out of logs | NIST SP 800-92 | § (log content guidance) |
Citations were retrieved via the NextPDF compliance engine (corpus manifest 1d05b7c4…d790b6); clause text is paraphrased, never quoted.
Threat model
Section titled “Threat model”| Threat | Control | Residual risk |
|---|---|---|
| SSRF via remote subresource | CSP default-src 'none' + CDP setBlockedURLs('*') | A Chrome engine bug that bypasses both barriers (defense-in-depth lowers risk, but does not eliminate it) |
| SSRF via meta-refresh navigation | Pre-Chrome validation rejects the tag | A new navigation vector not matched by the pattern |
| Resource exhaustion | Input size + base64 caps + timeout + 100-render restart | No per-host quota; pair with cgroup/ulimit |
| Renderer process compromise | Chrome sandbox when enabled | noSandbox: true removes this control entirely |
| Style break-out / injection | </style> stripping in defaultCss; CSP blocks script | Injection through a future vector that is not stripped |
FIPS-mode behavior
Section titled “FIPS-mode behavior”The bridge performs no cryptographic operations. It produces PDF bytes through Chrome and embeds them. Signing, encryption, and Federal Information Processing Standards (FIPS)-mode behavior are core/Premium concerns and are unaffected by Artisan.
See also
Section titled “See also”- /integrations/artisan/configuration/
- /integrations/artisan/chrome-renderer-setup/
- /integrations/artisan/troubleshooting/
- /integrations/artisan/production-usage/
- /integrations/artisan/overview/