Skip to content

Artisan security and operations

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.

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.

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.

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:

  1. 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 Chrome printToPDF to apply inline styles. Verified in src/Artisan/ChromeSecurityPolicy.php and asserted by ChromeSecurityPolicyTest::wrapHtmlIncludesNavigationCspDirectives.

  2. Chrome DevTools Protocol (CDP) transport block. Before content loads, ChromeHtmlRenderer issues Network.enable and then Network.setBlockedURLs with the pattern ['*']. This blocks every subresource URL at the Chrome DevTools Protocol transport layer, independent of CSP. Verified in src/Artisan/ChromeHtmlRenderer::blockAllNetworkRequests() and asserted by ChromeHtmlRendererTest::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/core docblock on writeHtmlChrome() 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 Artisan ChromeSecurityPolicy does 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.

ChromeSecurityPolicy::validate() runs before the bridge contacts Chrome and rejects:

CheckLimitRationale
HTML size> maxHtmlSize (default 5 MB)Resource-exhaustion bound (CWE Top 25 uncontrolled resource consumption)
Base64 data URIcapture group >= 13_000_000 bytesDecompression-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, noSandbox is false, 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: true launches 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 noSandbox only where unavoidable, and treat a --no-sandbox deployment 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.

These failure modes are enumerated from src/Artisan/Exception/ and the render/transport code:

ConditionSurfaced asSource
chrome-php/chrome library absentChromeNotAvailableException (with install command)BrowserPool::getBrowser()
HTML exceeds maxHtmlSizeRuntimeException (“exceeds maximum allowed size”)ChromeSecurityPolicy::validate()
Oversized base64 data URIRuntimeException (“oversized base64 data URI”)ChromeSecurityPolicy::validate()
Forbidden meta-refreshRuntimeException (“forbidden meta refresh redirect”)ChromeSecurityPolicy::validate()
Chrome launch / timeout / crashChromeRenderException (wrapping the cause)ChromeHtmlRenderer::render()
Chrome returned empty PDFChromeRenderException (“returned empty data”)ChromeHtmlRenderer::render()
Page has no content streamPdfParseExceptionPageImporter::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.

  • Input size: maxHtmlSize (default 5 MB) and the 13 MB base64 data-URI cap.
  • Time: renderTimeout seconds bounds both content load and CDP sync calls. CDP control commands use a fixed 5-second timeout.
  • Process: BrowserPool restarts Chrome every 100 renders to bound memory growth and closes the process on close() / 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.

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.

ClaimReferenceclause_idreference_id
Outbound requests from server components must be controlledOWASP ASVS 5.0§ (SSRF/outbound control)
SSRF = server retrieves a supplied URL without validating the destinationCWE Top 25 2025 (CWE-918)cwe_top25_2025#x28.x2.p2
SSRF (CWE-918) is a CWE Top 25 weaknessCWE Top 25 2025cwe_top25_2025#x1.p73
Uncontrolled resource consumption is a CWE Top 25 weaknessCWE 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-7SC-7
Network-layer deny of calls to arbitrary destinations is the strong SSRF controlOWASP Cheat Sheet Series (SSRF Prevention §Network layer)owasp_cheatsheet_series#x132.x2
Protect URL-fetching components against SSRFOWASP Cheat Sheet Series§ (SSRF prevention, URL-fetch tools)
Isolate untrusted-content rendering, least privilegeOWASP ASVS 5.0§ (sandbox / least privilege)
Log operational events; keep payloads out of logsNIST SP 800-92§ (log content guidance)

Citations were retrieved via the NextPDF compliance engine (corpus manifest 1d05b7c4…d790b6); clause text is paraphrased, never quoted.

ThreatControlResidual risk
SSRF via remote subresourceCSP 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 navigationPre-Chrome validation rejects the tagA new navigation vector not matched by the pattern
Resource exhaustionInput size + base64 caps + timeout + 100-render restartNo per-host quota; pair with cgroup/ulimit
Renderer process compromiseChrome sandbox when enablednoSandbox: true removes this control entirely
Style break-out / injection</style> stripping in defaultCss; CSP blocks scriptInjection through a future vector that is not stripped

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.

  • /integrations/artisan/configuration/
  • /integrations/artisan/chrome-renderer-setup/
  • /integrations/artisan/troubleshooting/
  • /integrations/artisan/production-usage/
  • /integrations/artisan/overview/