跳到內容

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() 會在注入前,將 </style>defaultCss 中移除,以防止內容從樣式區塊突圍進入指令稿情境(由 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/