跳转到内容

Artisan 安全与运维

这个 bridge 会在 Chrome 中渲染可能不受信任的 HTML,外围有两道独立的网络屏障,以及一套严格的内容策略。Chrome 的操作系统 sandbox 是另一道独立且可选的控制,也有明确限制。本页说明这些边界,并不声称它们是绝对边界。

一次渲染就是一次服务器端请求执行:应用程序把 HTML 交给浏览器引擎,而该引擎在默认情况下可以主动抓取资源。由不受信任输入驱动的对外抓取,就是服务器端请求伪造:CWE-918 将其定义为服务器在未充分确认请求是否发送到预期目的地的情况下,获取指定 URL 内容。SSRF(CWE-918)属于 CWE Top 25 弱点。OWASP ASVS 要求服务器组件的对外请求必须受控,而不是隐式放行。OWASP SSRF Prevention Cheat Sheet 将「在网络层拒绝调用任意目的地」视为强控制。下方默认拒绝的网络态势,就是这个 bridge 对该要求的回应。NIST SP 800-53 SC-7 描述的「全部拒绝、例外放行」边界原则,正是这个 bridge 在传输层采用的原则。

传给 bridge 的 HTML 完全在进程内以及本机 Chrome 实例内处理。bridge 本身不会发出任何对外网络调用,并会阻止 Chrome 发出任何对外调用(见下方网络模型),因此输入内容不会经由 renderer 离开主机。输入中的 PII 会被渲染进你生成的 PDF——请采用与输入相同的数据驻留控制来处理输出。bridge 不会把输入或输出写入磁盘;持久化由调用端负责。

ChromeHtmlRendererBrowserPool 接受可选的 PSR-3 LoggerInterface。这个 bridge 只记录运维元数据:输入字节长度、目标宽度和高度、输出字节长度、测得的内容高度、用于启动浏览器的可执行文件路径、包含渲染次数的重启通知,以及关闭事件。它不会记录 HTML 内容、渲染后的字节,或提取出的文本。这符合 NIST SP 800-92 的指引:记录运维事件,同时避免敏感负载进入日志。可执行文件路径会被记录;请将其视为非敏感的部署元数据。日志调用结构由 tests/Unit/Artisan/ChromeHtmlRendererTest.php::renderLogsDebugWithSizeWidthHeightAndPdfSizetests/Unit/Artisan/BrowserPoolTest.php::getBrowserLogsInfoOnLaunchWithBinaryPath 断言。

这个 bridge 会套用两道独立屏障,即使其中一道被绕过,也不会暴露主机:

  1. **Content-Security-Policy。**每次渲染都会由 ChromeSecurityPolicy::wrapHtml() 包装成一份包含以下内容的文档:

    default-src 'none'; style-src 'unsafe-inline'; img-src data:;
    base-uri 'none'; form-action 'none'; frame-ancestors 'none';
    navigate-to 'none';

    default-src 'none' 拒绝所有资源来源。img-src data: 只允许内嵌图像。navigate-to 'none' 阻止客户端导航。style-src 'unsafe-inline' 是为了让 Chrome printToPDF 能应用内嵌样式所需的唯一放宽项。该行为在 src/Artisan/ChromeSecurityPolicy.php 中验证,并由 ChromeSecurityPolicyTest::wrapHtmlIncludesNavigationCspDirectives 断言。

  2. **CDP 传输阻挡。**在加载内容之前,ChromeHtmlRenderer 会先发送 Network.enable,然后以 Network.setBlockedURLs 搭配模式 ['*'],不论 CSP 如何,都会在 Chrome DevTools Protocol 传输层阻止每一个子资源 URL。该行为在 src/Artisan/ChromeHtmlRenderer::blockAllNetworkRequests() 中验证,并由 ChromeHtmlRendererTest::renderAutoFitsHeightAndBlocksNetworkRequests 断言(它会检查确切的 CDP 方法顺序与 ['urls' => ['*']] 参数)。这正是 OWASP SSRF 指引建议作为最强控制的网络层阻挡,也与 NIST SP 800-53 SC-7 的传输层全部拒绝一致。

结果是:输入中的远端 <img>、样式表、字体、脚本或 iframe URL 都不会加载。这个 bridge 没有实现域名允许列表或私有 IP 过滤,因为它不需要这些机制——它根本不允许任何对外子资源抓取。

漂移备注:nextpdf/corewriteHtmlChrome() 上的 docblock 写着 Chrome「会抓取外部资源」,并建议设置一套策略来「封锁私有 IP 范围并限制允许的域名」。那描述的是一种可配置允许列表的模型。实际发布的 Artisan ChromeSecurityPolicy 并未提供允许列表,而是无条件封锁所有子资源请求。权威依据是代码,而不是 core 的 docblock。此漂移已记录给 core 文档团队。

ChromeSecurityPolicy::validate() 会在接触 Chrome 前执行,并拒绝:

检查项上限理由
HTML 大小> maxHtmlSize(默认 5 MB)资源耗尽的边界(CWE Top 25 失控资源消耗)
Base64 数据 URI捕获组 >= 13_000_000 字节解压缩炸弹边界
<meta http-equiv="refresh">任何形式(不区分大小写,single/double 引号)阻止面向内部端点的客户端重定向——一种 SSRF 导航向量

封锁 meta-refresh 是明确的 SSRF 加固:否则,攻击者的 HTML 可能会在 printToPDF 前把 Chrome 重定向到云端元数据端点。边界行为由 ChromeSecurityPolicyTest 全面断言(validateThrowsOnOversizedHtmlvalidateRejectsMetaRefreshRedirectvalidateRejectsMetaRefreshCaseInsensitivevalidateRejectsMetaRefreshWithSingleQuotesvalidateRejectsOversizedBase64DataUrivalidateRejectsBase64DataUriAtExactThreshold)。

此外,ChromeSecurityPolicy::wrapHtml() 会在注入前从 defaultCss 中移除 </style>,防止样式块突围进入脚本上下文(由 ChromeSecurityPolicyTest::wrapHtmlStripsStyleClosingTagsFromDefaultCss 断言)。

Chrome 操作系统 sandbox 是与上述网络屏障不同的另一道控制,而这个 bridge 并不保证它一定生效。

  • 默认情况下,noSandboxfalse,因此 Chrome 会在启动时启用自身的 sandbox。这个 bridge 并未实现 sandbox;它依赖 Chrome 可执行文件的 sandbox,而后者取决于主机内核支持。
  • 设置 noSandbox: true 会以 --no-sandbox 启动 Chrome。这会移除 Chrome 的进程隔离 sandbox。它是为 sandbox 无法初始化的容器提供的。隔离程度会实质降低:renderer 一旦被攻陷,就不再受 Chrome 的 sandbox 围堵。
  • 无论是否启用 sandbox,这个 bridge 的网络屏障(CSP 加 CDP 阻挡)都会保持生效,但它们不能替代进程隔离。OWASP ASVS 的最小权限指引适用于此:以非 root 用户运行 Chrome、将其置于受限容器中、仅在无法避免时才使用 noSandbox,并将 --no-sandbox 部署视为对输入有更高信任要求。

本文并不声称这个 bridge「默认安全」、「防篡改」,也不声称停用 sandbox 是安全的。它说明的是现有控制及其边界。如何部署具备 sandbox 能力的容器,请参阅 /integrations/artisan/chrome-renderer-setup/ 页面。

以下列举来自 src/Artisan/Exception/ 与 render/transport 代码:

条件呈现为来源
chrome-php/chrome 库缺失ChromeNotAvailableException(附安装指令)BrowserPool::getBrowser()
HTML 超过 maxHtmlSizeRuntimeException(消息「exceeds maximum allowed size」,即超过允许的最大大小)ChromeSecurityPolicy::validate()
过大的 base64 数据 URIRuntimeException(消息「oversized base64 data URI」,即 base64 数据 URI 过大)ChromeSecurityPolicy::validate()
被禁止的 meta-refreshRuntimeException(消息「forbidden meta refresh redirect」,即禁止的 meta-refresh 重定向)ChromeSecurityPolicy::validate()
Chrome 启动/超时/崩溃ChromeRenderException(包装起因)ChromeHtmlRenderer::render()
Chrome 返回空白 PDFChromeRenderException(消息「returned empty data」,即返回了空白数据)ChromeHtmlRenderer::render()
页面没有内容流PdfParseExceptionPageImporter::import()

渲染内部抛出的 ChromeRenderException 会原样重新抛出。任何其他 Throwable 都会被包装成 ChromeRenderException,并保留前一个异常(由 ChromeHtmlRendererTest::renderRethrowsChromeRenderExceptionWithoutWrapping::renderWrapsUnexpectedThrowablesWithChromeRenderException 断言)。即使失败,Chrome 页面也始终会在 finally 块中关闭。

  • 输入大小maxHtmlSize(默认 5 MB)以及 13 MB 的 base64 数据 URI 上限。
  • 时间renderTimeout 秒数同时限定内容加载与 CDP 同步调用。CDP 控制指令使用固定的 5 秒超时。
  • 进程BrowserPool 每 100 次渲染会重启 Chrome,以限制内存增长,并在 close()/销毁时关闭进程。

这些是边界,而不是配额。对于任何暴露于不受信任输入的路径,仍建议结合主机层级的资源限制(cgroup、ulimit、请求预算),这与 CWE Top 25 的资源消耗指引一致。

注入一个 PSR-3 logger,以采集:渲染开始(大小、宽度、高度)、渲染完成(输出大小、内容高度)、浏览器启动(可执行文件路径)、浏览器重启(渲染次数)、浏览器关闭(渲染次数)。这些是唯一发出的事件,而且都不包含任何负载内容。请用它们来做延迟 SLO 与重启率告警。

主张参考依据clause_id(子句 ID)reference_id(参考 ID)
服务器组件的对外请求必须受控OWASP ASVS 5.0§(SSRF/对外控制)
SSRF = 服务器在未验证目的地的情况下获取指定 URLCWE Top 25 2025 标准(CWE-918)cwe_top25_2025#x28.x2.p2
SSRF(CWE-918)是一项 CWE Top 25 弱点CWE Top 25 2025 标准cwe_top25_2025#x1.p73
失控资源消耗是一项 CWE Top 25 弱点CWE Top 25 2025 标准(CWE-400)cwe_top25_2025#x19.x2.p2
默认拒绝的边界保护(例外放行)NIST SP 800-53 Rev 5 SC-7 标准SC-7
在网络层拒绝调用任意目的地是强 SSRF 控制OWASP Cheat Sheet Series(SSRF Prevention §网络層)owasp_cheatsheet_series#x132.x2
保护抓取 URL 的组件以抵御 SSRFOWASP Cheat Sheet Series 指引§(SSRF 防护、抓取 URL 工具)
隔离不受信任内容渲染、最小权限OWASP ASVS 5.0§(sandbox/最小权限)
记录运维事件;避免负载进入日志NIST SP 800-92§(记录内容指引)

这些引用通过 NextPDF 合规性引擎取得(语料库信息清单 1d05b7c4…d790b6);条文内容均为改写,绝不逐字引用。

威胁控制残余风险
经由远端子资源的 SSRFCSP default-src 'none' 加 CDP setBlockedURLs('*')同时绕过两道屏障的 Chrome 引擎漏洞(纵深防御只能降低风险,无法消除风险)
经由 meta-refresh 导航的 SSRF进入 Chrome 前的验证会拒绝该标签模式未覆盖的新导航向量
资源耗尽输入大小加 base64 上限加超时加每 100 次渲染重启无每主机配额;请结合 cgroup/ulimit
renderer 进程被攻陷Chrome sandbox 启用时noSandbox: true 会完全移除这道控制
样式突围/注入</style>defaultCss 中移除;CSP 阻挡脚本通过未来某个未被移除的向量进行注入

这个 bridge 不执行任何密码学运算。它通过 Chrome 生成 PDF 字节并将其内嵌。签名、加密与 FIPS 模式行为属于 core/Premium 范畴,不受 Artisan 影响。

  • 配置:/integrations/artisan/configuration/
  • Chrome renderer(渲染器)设置:/integrations/artisan/chrome-renderer-setup/
  • 故障排查:/integrations/artisan/troubleshooting/
  • 生产环境使用:/integrations/artisan/production-usage/
  • 总览:/integrations/artisan/overview/