Перейти к содержимому

Безопасность и эксплуатация

Этот мост отправляет ваш HTML через сетевую границу в браузерный движок. На этой странице описаны средства контроля, защищающие эту границу; источником истины остаётся исходный код. Если средство контроля ссылается на стандарт, используется та же ссылка, что указана в docblock кода. Страница повторяет утверждения кода, но не воспроизводит нормативные формулировки.

В docblock пакета перечислены угрозы, от которых он защищает:

  • XSS-to-PDF — межсайтовый скриптинг (XSS) через вредоносную разметку, выполняемую во время отрисовки формата переносимых документов (PDF).
  • SSRF — подделка запросов на стороне сервера (SSRF), вызванная разметкой или целевым унифицированным указателем ресурса (URL), из-за которого запрос уходит на внутренний адрес.
  • Исчерпание ресурсов — слишком большой ввод или бомба декомпрессии.
  • Перепривязка DNS — перепривязка системы доменных имён (DNS), при которой имя узла проходит проверку, а во время подключения разрешается в частный адрес.
  • Перехват TLS на пути следования — перехват защиты транспортного уровня (TLS) через подменённый сертификат на маршруте к Worker.

Для каждой угрозы ниже указано конкретное проверяемое средство контроля.

Контроль ввода (до того как запрос покинет PHP)

Заголовок раздела «Контроль ввода (до того как запрос покинет PHP)»

CloudflareSecurityPolicy::validate() выполняется до формирования любого запроса:

Средство контроляПоведениеИсточник ограничения
Ограничение размераОтклоняет HTML больше, чем maxHtmlSizeCloudflareRendererConfig, по умолчанию 5000000 байт
Защита от бомбы декомпрессии в Base64Оценивает декодированный размер каждого URI data:…;base64,…; отклоняет значения, равные верхнему пределу или превышающие егоMAX_DATA_URI_BYTES = 13631488
Запрет meta-refreshОтклоняет любой <meta http-equiv="refresh"> без учёта регистрарегулярное выражение в CloudflareSecurityPolicy

При нарушении возникает RuntimeException с сообщением, где указаны нарушающее значение и ограничение. Запрет meta-refresh нужен потому, что директива refresh может запустить навигацию внутри страницы, которую отрисовывает Worker: это вектор SSRF в содержимом, а не в URL.

Политика безопасности HTML из nextpdf/core (HtmlSecurityPolicyInterface, по умолчанию DefaultHtmlSecurityPolicy) применяется на уровне разбора и дополняет проверки транспортного уровня, описанные выше. Получите её через getHtmlSecurityPolicy(). Собственную политику передайте через конструктор.

Контроль назначения (SSRF и перепривязка DNS)

Заголовок раздела «Контроль назначения (SSRF и перепривязка DNS)»

CloudflareSecurityPolicy::validateWorkerUrl():

  1. Отклоняет URL, который не удаётся разобрать или в котором нет scheme/host (Invalid Worker URL).
  2. Отклоняет любую схему, кроме HTTPS (Worker URL must use HTTPS).
  3. Для узла, заданного IP-литералом, отклоняет частные или зарезервированные диапазоны с помощью FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE из PHP. На практике это отклоняет частное пространство RFC 1918, петлевые адреса и канально-локальные адреса RFC 3927. Тесты явно проверяют отклонение 192.168.x, 127.0.0.1 и 169.254.x. Принадлежность к диапазону определяет расширение filter в PHP; пакет не привязывает это решение к какой-либо статье. RFC 1918 и RFC 3927 приведены здесь описательно, как общеизвестные определения этих диапазонов.
  4. Для имени узла разрешает все записи A и AAAA с помощью dns_get_record() (а не gethostbyname(), который возвращает только первый ответ) и отклоняет узел, если любой разрешённый адрес является частным или зарезервированным.

Разрешение всех записей используется намеренно. В docblock класса это описано как защита от узла, который возвращает несколько записей: при разрешении одной записи может быть выбран публичный адрес, а при последующем подключении — частный. Это соответствует OWASP SSRF Prevention Cheat Sheet: разрешать для домена и записи A, и записи AAAA, а проверку на непубличный адрес применять ко всему набору результатов.

validateWorkerUrl() возвращает проверенный набор IP-адресов. Непосредственно перед отправкой средство отрисовки вызывает assertPinsStillValid(). Этот вызов повторно разрешает узел и отклоняет вновь появившийся IP-адрес (Worker URL DNS answer changed since validation — possible DNS rebinding attack). Так закрывается окно между временем проверки и временем использования (time-of-check / time-of-use) между валидацией и подключением.

Когда есть проверенный набор IP-адресов или набор закреплённых ключей SPKI (Subject Public Key Info) и передана фабрика ResponseFactory рекомендации PHP Standards Recommendation 17 (PSR-17), средство отрисовки использует Transport\PinnedCurlTransport вместо внедрённого клиента рекомендации PHP Standards Recommendation 18 (PSR-18). Транспорт применяет эти средства контроля на уровне дескриптора cURL:

  • Закреплённый DNSCURLOPT_RESOLVE привязывает host:port к проверенному набору IP-адресов, поэтому libcurl не выполняет собственный поиск во время подключения. Благодаря этой привязке пользовательская проверка DNS применяется к фактическому подключению; без неё libcurl мог бы разрешить другой адрес.
  • Закрепление открытого ключа TLSCURLOPT_PINNEDPUBLICKEY устанавливается из объединённого набора закреплённых ключей. Это соответствует RFC 7469 §2.6: закреплённое подключение принимается, когда набор отпечатков SPKI, предъявленный сервером, пересекается с настроенным набором закреплённых ключей, а сбой проверки закрепления неустраним. Строки закрепления нормализуются из формы sha256/<base64> в форму cURL sha256//<base64>; недопустимый закреплённый ключ вызывает InvalidSpkiPinException.
  • Проверка TLS включенаCURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2.
  • Без автоматических перенаправленийCURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 0. Ответ 3xx передаётся на уровень политики; libcurl не переходит на непроверенный узел. В docblock класса указано, что это сделано намеренно: перенаправления проверяются повторно, а не выполняются молча.
  • Жёсткий тайм-аутCURLOPT_TIMEOUT устанавливается из renderTimeout (по умолчанию 30 секунд).

Ошибка cURL или тело не строкового типа вызывает CloudflareRenderException с номером и сообщением ошибки cURL.

Рекомендации по эксплуатации закрепления

Заголовок раздела «Рекомендации по эксплуатации закрепления»

Конфигурация содержит pinnedPublicKeys и отдельное backupPublicKeys. RFC 7469 §2.5 описывает резервный закреплённый ключ как отпечаток вторичной, ещё не развёрнутой пары ключей, которая хранится автономно, и рассматривает его как основной способ восстановления при непреднамеренном сбое проверки закрепления. Сохраняйте хотя бы один резервный закреплённый ключ, чтобы ротация сертификата не вывела конечную точку из строя. Отдельное поле позволяет проверять ротацию независимо. На практике:

  • Закрепляйте SPKI листового сертификата или промежуточного сертификата, ротацию которого вы контролируете.
  • Перед ротацией всегда настраивайте резервный закреплённый ключ для следующего сертификата.
  • Пустой набор закреплённых ключей отключает закрепление; используйте это только со стабильной и известной цепочкой сертификатов. Закрепление включается через конфигурацию.
  • Запрос к Worker содержит Authorization: Bearer <apiToken>. apiToken помечен #[SensitiveParameter], поэтому трассировки стека его скрывают. Проверка доступности отправляет тот же заголовок Bearer в запросе протокола передачи гипертекста (HTTP) HEAD.
  • Ключи доступа Cloudflare R2 (accessKeyId, secretAccessKey) помечены #[SensitiveParameter] и используются только для получения ключа подписи Amazon Web Services (AWS) Signature V4.
  • ApiKeyValidator сравнивает ключи с помощью hash_equals() (устойчиво к атакам по времени) и поддерживает хранение хешированных ключей по алгоритму безопасного хеширования 256 (SHA-256) через validateHashed().
  • Объекты конфигурации помечены final readonly — однажды заданный секрет нельзя изменить.
  • Берите секреты из переменных окружения или менеджера секретов. Никогда не фиксируйте их в репозитории. Пакет следует общей базовой линии безопасности NextPDF: PHPStan Level 10, declare(strict_types=1) в каждом файле, без eval()/exec(), GitHub Actions закреплены по SHA.
  • Он не указывает платформенные ограничения Cloudflare (время ЦП Worker, память, верхний предел тела запроса или количество подзапросов). Единственные ограничения размера и времени, указанные в этой документации, — это те, которые пакет применяет сам, перечисленные выше и в /integrations/cloudflare/configuration/. По платформенным ограничениям обращайтесь к официальной документации Cloudflare и реализации вашего собственного Worker.
  • Он не подписывает PDF и не делает никаких заявлений о соответствии подписей. Когда требуются подписи, выполните отрисовку здесь, а затем подпишите документ движком. NextPDF Pro предоставляет только подписание усовершенствованной электронной подписью PDF (PAdES) B-B; профили долговременной проверки — возможность Enterprise, они вне области применения этого моста.
  • Он не сертифицирует и не гарантирует конвейер, а также не делает его “защищённым от вмешательства”. Он реализует только конкретные, проверяемые по исходному коду средства контроля, описанные на этой странице.
СимптомПервая проверка
Worker URL must use HTTPSПроверьте схему настроенного workerUrl.
private or reserved IPDNS-записи имени узла Worker; ищите запись, которая разрешается в пространство RFC 1918 / петлевое / RFC 3927.
DNS answer changed since validationНестабильность DNS или попытка перепривязки; выполните повторное разрешение и проверьте полный набор записей.
cURL transport errorСетевой путь, цепочка TLS и — если закреплённые ключи заданы — наличие SPKI предъявленного сертификата в наборе закреплённых ключей.
Отрисовка завершается ошибкой сразу после ротации сертификатаНабор закреплённых ключей без соответствующего резервного закреплённого ключа. Добавьте новый SPKI как резервный до ротации.
is not installed / no LocalRendererFactoryInterfaceРезервный режим включён, но не подключена ни одна фабрика, либо отсутствует nextpdf/artisan.
Отказы из-за ограничения частоты несогласованы между узламиОграничитель в памяти работает в пределах процесса; разместите перед ним общее хранилище.

Сообщайте об уязвимостях через GitHub Security Advisories или контакт по безопасности из файла SECURITY.md репозитория. Не оформляйте проблемы безопасности как публичные issue на GitHub.

  • /integrations/cloudflare/overview/ — почему этот пакет построен вокруг границы доверия.
  • /integrations/cloudflare/configuration/ — поля наборов закреплённых ключей и ограничений.
  • /integrations/cloudflare/troubleshooting/ — полное сопоставление сбоев и исключений.