Bỏ qua để đến nội dung

Bảo mật và vận hành

Cầu nối này gửi Hypertext Markup Language (HTML) của bạn qua một ranh giới mạng đến một browser engine. Trang này ghi lại các biện pháp kiểm soát bảo vệ ranh giới đó, lấy mã nguồn làm cơ sở xác thực. Khi một biện pháp kiểm soát trích dẫn một tiêu chuẩn, đó chính là trích dẫn được khai báo trong docblock của mã. Trang này diễn đạt lại các khẳng định trong mã; nó không tái dựng câu chữ chuẩn tắc.

Các docblock của gói nêu rõ những mối đe dọa mà gói phòng vệ:

  • XSS-to-PDF — Cross-site scripting (XSS) thông qua markup độc hại chạy trong quá trình kết xuất Portable Document Format (PDF).
  • SSRF — Server-side request forgery (SSRF) do markup hoặc một Uniform Resource Locator (URL) đích gửi yêu cầu đến địa chỉ nội bộ.
  • Cạn kiệt tài nguyên — Đầu vào quá lớn hoặc bom giải nén.
  • DNS rebinding — DNS rebinding (Domain Name System), khi một hostname vượt qua bước xác thực rồi sau đó phân giải thành một địa chỉ riêng tư vào thời điểm kết nối.
  • Chặn TLS trên đường truyền — Chặn Transport Layer Security (TLS) trên đường truyền bằng một chứng chỉ bị thay thế trên đường đến Worker.

Mỗi mối đe dọa đều có một biện pháp kiểm soát cụ thể, có thể kiểm thử, được trình bày bên dưới.

Kiểm soát đầu vào (trước khi yêu cầu rời khỏi PHP)

Phần tiêu đề “Kiểm soát đầu vào (trước khi yêu cầu rời khỏi PHP)”

CloudflareSecurityPolicy::validate() chạy trước khi bất kỳ yêu cầu nào được dựng:

Biện pháp kiểm soátHành viNguồn giới hạn
Giới hạn kích thướcTừ chối HTML lớn hơn maxHtmlSizeCloudflareRendererConfig, mặc định 5000000 byte
Bộ bảo vệ bom giải nén Base64Ước tính kích thước sau giải mã của mọi URI data:…;base64,…; từ chối các giá trị bằng hoặc vượt ngưỡng trầnMAX_DATA_URI_BYTES = 13631488
Cấm meta-refreshTừ chối mọi <meta http-equiv="refresh">, không phân biệt chữ hoa chữ thườngregex trong CloudflareSecurityPolicy

Một vi phạm sẽ ném RuntimeException kèm thông báo nêu rõ giá trị vi phạm và giới hạn. Lệnh cấm meta-refresh tồn tại vì một chỉ thị refresh có thể khởi động điều hướng bên trong trang mà Worker kết xuất — một vector SSRF nằm trong nội dung, không phải trong URL.

Chính sách bảo mật HTML từ nextpdf/core (HtmlSecurityPolicyInterface, mặc định DefaultHtmlSecurityPolicy) chạy ở lớp phân tích cú pháp và bổ trợ cho các bước kiểm tra ở lớp transport nêu trên. Hãy lấy chính sách này bằng getHtmlSecurityPolicy(). Có thể chèn một chính sách tùy chỉnh thông qua constructor.

Kiểm soát đích đến (SSRF và DNS rebinding)

Phần tiêu đề “Kiểm soát đích đến (SSRF và DNS rebinding)”

CloudflareSecurityPolicy::validateWorkerUrl():

  1. Từ chối một URL không phân tích được hoặc thiếu scheme/host (Invalid Worker URL).
  2. Từ chối mọi scheme khác ngoài HTTPS (Worker URL must use HTTPS).
  3. Với host là IP literal, từ chối các dải riêng tư hoặc dải dành riêng bằng FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE của PHP. Trên thực tế, điều này từ chối không gian riêng tư RFC 1918, loopback và các địa chỉ link-local RFC 3927. Các bài kiểm thử bao phủ rõ ràng việc từ chối 192.168.x, 127.0.0.1169.254.x. Phần mở rộng filter của PHP quyết định một địa chỉ thuộc dải nào; gói này không gắn quyết định đó với một điều khoản cụ thể. RFC 1918 và RFC 3927 được nêu tên ở đây theo nghĩa mô tả, như những định nghĩa quen thuộc cho các dải đó.
  4. Với hostname, phân giải tất cả bản ghi A và AAAA bằng dns_get_record() (không phải gethostbyname(), vốn chỉ trả về câu trả lời đầu tiên) và từ chối host nếu bất kỳ địa chỉ nào được phân giải là riêng tư hoặc dành riêng.

Việc phân giải toàn bộ bản ghi là có chủ đích. Docblock của lớp ghi lại rằng đây là cơ chế phòng vệ trước host trả về nhiều bản ghi, trong đó việc tra cứu chỉ một bản ghi có thể chọn địa chỉ công khai, còn kết nối sau đó lại chọn địa chỉ riêng tư. Điều này khớp với OWASP SSRF Prevention Cheat Sheet: phân giải cả câu trả lời A lẫn AAAA cho domain và áp dụng bước kiểm tra địa chỉ không công khai cho toàn bộ tập kết quả.

validateWorkerUrl() trả về tập IP đã được thẩm định. Ngay trước khi gửi, bộ kết xuất gọi assertPinsStillValid(). Lệnh gọi này phân giải lại host và từ chối mọi IP mới xuất hiện (Worker URL DNS answer changed since validation — possible DNS rebinding attack). Điều này đóng cửa sổ time-of-check / time-of-use giữa thời điểm xác thực và thời điểm kết nối.

Khi có một tập IP đã thẩm định hoặc một tập ghim Subject Public Key Info (SPKI) có cung cấp một ResponseFactory PHP Standards Recommendation 17 (PSR-17), bộ kết xuất dùng Transport\PinnedCurlTransport thay cho client PHP Standards Recommendation 18 (PSR-18) được tiêm vào. Transport thực thi các biện pháp kiểm soát này ở lớp cURL handle:

  • Ghim DNSCURLOPT_RESOLVE gắn host:port với tập IP đã thẩm định, nên libcurl không tự tra cứu tại thời điểm kết nối. Sự gắn kết này khiến bước kiểm tra DNS ở userland áp dụng cho chính kết nối thực tế; nếu không có nó, libcurl có thể phân giải ra một địa chỉ khác.
  • Ghim khóa công khai TLSCURLOPT_PINNEDPUBLICKEY được đặt từ tập ghim hợp nhất. Điều này tuân theo RFC 7469 §2.6: kết nối có ghim được chấp nhận khi tập dấu vân tay SPKI mà máy chủ trình ra giao với tập ghim đã cấu hình, và lỗi xác thực ghim là không thể khôi phục. Các chuỗi ghim được chuẩn hóa từ dạng sha256/<base64> sang dạng sha256//<base64> của cURL; ghim sai định dạng sẽ ném InvalidSpkiPinException.
  • Bật xác minh TLSCURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2.
  • Không chuyển hướng tự độngCURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 0. Phản hồi 3xx được đưa lên lớp chính sách thay vì để libcurl đi theo đến một host chưa được thẩm định. Docblock của lớp nêu rằng đây là chủ ý, để các chuyển hướng được xác thực lại thay vì bị âm thầm đi theo.
  • Thời gian chờ cứngCURLOPT_TIMEOUT được đặt từ renderTimeout (mặc định 30 giây).

Lỗi cURL hoặc body không phải chuỗi sẽ ném CloudflareRenderException kèm số lỗi và thông báo của cURL.

Cấu hình mang theo pinnedPublicKeys và một backupPublicKeys riêng. RFC 7469 §2.5 mô tả ghim dự phòng là dấu vân tay cho cặp khóa thứ hai chưa triển khai, được giữ ngoại tuyến, và xem đó là đường khôi phục chính khi xác thực ghim thất bại ngoài ý muốn. Hãy giữ ít nhất một ghim dự phòng để việc xoay vòng chứng chỉ không làm hỏng endpoint. Trường riêng này cho phép bạn kiểm chứng một lần xoay vòng một cách độc lập. Về mặt vận hành:

  • Hãy ghim SPKI của chứng chỉ lá hoặc của một chứng chỉ trung gian mà bạn kiểm soát được việc xoay vòng.
  • Luôn cấu hình ghim dự phòng cho chứng chỉ kế tiếp trước khi xoay vòng.
  • Một tập ghim rỗng sẽ vô hiệu hóa việc ghim; chỉ dùng cách đó với một chuỗi chứng chỉ ổn định, đã biết. Việc ghim là tùy chọn được bật qua cấu hình.
  • Yêu cầu Worker mang theo Authorization: Bearer <apiToken>. apiToken#[SensitiveParameter], nên các stack trace sẽ ẩn giá trị này. Phép thăm dò khả năng tiếp cận gửi cùng header bearer đó trên một HEAD Hypertext Transfer Protocol (HTTP).
  • Các access key Cloudflare R2 (accessKeyId, secretAccessKey) là #[SensitiveParameter] và chỉ được dùng để dẫn xuất khóa ký Amazon Web Services (AWS) Signature V4.
  • ApiKeyValidator so sánh các khóa bằng hash_equals() (an toàn về thời gian) và hỗ trợ lưu trữ khóa đã băm Secure Hash Algorithm 256 (SHA-256) thông qua validateHashed().
  • Các đối tượng cấu hình là final readonly — một bí mật đã đặt thì không thể thay đổi.
  • Hãy lấy bí mật từ biến môi trường hoặc trình quản lý bí mật. Đừng bao giờ commit chúng. Gói tuân theo nền tảng bảo mật rộng hơn của NextPDF: PHPStan Level 10, declare(strict_types=1) trên mọi tệp, không dùng eval()/exec(), GitHub Actions được ghim theo SHA.
  • Gói không nêu bất kỳ giới hạn nền tảng Cloudflare nào (thời gian CPU của Worker, bộ nhớ, mức trần kích thước body của yêu cầu, hay số lượng subrequest). Những giới hạn về kích thước và thời gian duy nhất mà tài liệu này nêu là những giới hạn do chính gói thực thi, được liệt kê ở trên và trong /integrations/cloudflare/configuration/. Về các giới hạn nền tảng, hãy tham khảo tài liệu chính thức của Cloudflare và bản triển khai Worker của riêng bạn.
  • Gói không ký PDF và không đưa ra bất kỳ tuyên bố nào về tuân thủ chữ ký. Khi cần chữ ký, hãy kết xuất tại đây, rồi ký bằng engine. NextPDF Pro chỉ cung cấp ký PDF Advanced Electronic Signatures (PAdES) B-B; các hồ sơ xác thực dài hạn là một năng lực của bản Enterprise và nằm ngoài phạm vi của cầu nối này.
  • Gói không chứng nhận, bảo đảm, hay làm cho pipeline trở nên “chống giả mạo”. Gói chỉ triển khai những biện pháp kiểm soát cụ thể, có thể kiểm chứng qua mã nguồn, được mô tả trên trang này.
Triệu chứngKiểm tra trước tiên
Worker URL must use HTTPSHãy kiểm tra scheme của workerUrl đã cấu hình.
private or reserved IPCác bản ghi DNS của hostname Worker; hãy tìm bản ghi phân giải vào không gian RFC 1918 / loopback / RFC 3927.
DNS answer changed since validationDNS không ổn định hoặc một nỗ lực rebinding; hãy phân giải lại và kiểm tra toàn bộ tập bản ghi.
cURL transport errorĐường truyền mạng, chuỗi TLS, và — nếu có đặt ghim — kiểm tra xem SPKI của chứng chỉ được phục vụ có còn nằm trong tập ghim hay không.
Các lần kết xuất thất bại ngay sau một lần xoay vòng chứng chỉTập ghim không có ghim dự phòng phù hợp. Hãy thêm SPKI mới làm ghim dự phòng trước khi xoay vòng.
is not installed / no LocalRendererFactoryInterfaceCơ chế dự phòng được bật nhưng không có factory nào được nối, hoặc nextpdf/artisan vắng mặt.
Việc từ chối do giới hạn tốc độ không nhất quán giữa các nodeBộ giới hạn trong bộ nhớ hoạt động theo từng tiến trình; hãy đặt một kho lưu trữ dùng chung ở phía trước nó.

Hãy báo cáo lỗ hổng qua GitHub Security Advisories hoặc đầu mối liên hệ bảo mật trong SECURITY.md của repository. Đừng gửi vấn đề bảo mật dưới dạng GitHub issue công khai.

  • /integrations/cloudflare/overview/ — vì sao gói này được định hình quanh ranh giới.
  • /integrations/cloudflare/configuration/ — các trường tập ghim và giới hạn.
  • /integrations/cloudflare/troubleshooting/ — ánh xạ đầy đủ từ lỗi sang ngoại lệ.