Aller au contenu

Sécurité et exploitation

Ce pont envoie ton HTML à travers une frontière réseau vers un moteur de navigateur. Cette page documente chaque contrôle qui protège cette frontière, tel qu’il est décrit dans le code source. Lorsqu’un contrôle cite une norme, la citation correspond à ce que déclare le docblock du code lui-même. Cette page reformule l’assertion du code et ne reconstitue pas le texte normatif.

Les docblocks du paquet nomment eux-mêmes les menaces contre lesquelles il se défend :

  • XSS-vers-PDF — un balisage hostile qui s’exécute pendant le rendu.
  • SSRF — un balisage ou une URL de destination qui déclenche une requête vers une adresse interne.
  • Épuisement des ressources — une entrée surdimensionnée ou une bombe de décompression.
  • Rebinding DNS — un nom d’hôte qui passe la validation, puis se résout vers une adresse privée au moment de la connexion.
  • Interception TLS sur le chemin — un certificat substitué sur le chemin vers le Worker.

Chacune est traitée par un contrôle spécifique et testable ci-dessous.

Contrôles d’entrée (avant que la requête ne quitte PHP)

Section intitulée « Contrôles d’entrée (avant que la requête ne quitte PHP) »

CloudflareSecurityPolicy::validate() s’exécute avant la construction de toute requête :

ContrôleComportementSource de la limite
Plafond de tailleRejette un HTML plus volumineux que maxHtmlSizeCloudflareRendererConfig, valeur par défaut 5000000 octets
Garde-fou contre les bombes de décompression Base64Estime la taille décodée de chaque URI data:…;base64,… ; rejette dès que la limite est atteinte ou dépasséeMAX_DATA_URI_BYTES = 13631488
Interdiction du meta-refreshRejette tout <meta http-equiv="refresh">, sans tenir compte de la casseexpression régulière dans CloudflareSecurityPolicy

Une violation lève une RuntimeException avec un message qui nomme la valeur fautive et la limite. L’interdiction du meta-refresh existe parce qu’une directive de rafraîchissement peut déclencher une navigation depuis l’intérieur de la page rendue par le Worker — un vecteur SSRF qui se trouve dans le contenu, pas dans l’URL.

La politique de sécurité HTML de nextpdf/core (HtmlSecurityPolicyInterface, par défaut DefaultHtmlSecurityPolicy) intervient à la couche d’analyse syntaxique et complète les vérifications de la couche transport ci-dessus. Récupère-la avec getHtmlSecurityPolicy(). Injecte une politique personnalisée via le constructeur.

CloudflareSecurityPolicy::validateWorkerUrl() :

  1. Rejette une URL qui ne peut pas être analysée ou à laquelle il manque un scheme/host (Invalid Worker URL).
  2. Rejette tout schéma non-HTTPS (Worker URL must use HTTPS).
  3. Pour un hôte exprimé en IP littérale, rejette les plages privées ou réservées en utilisant le FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE de PHP. En pratique, cela rejette l’espace privé RFC 1918, le bouclage local et les adresses lien-local RFC 3927 — les tests exercent explicitement les rejets de 192.168.x, 127.0.0.1 et 169.254.x. L’appartenance à une plage est décidée par l’extension filter de PHP, et non par une clause que ce paquet épingle ; RFC 1918 et RFC 3927 sont nommées ici à titre descriptif, comme les définitions bien connues de ces plages.
  4. Pour un nom d’hôte, résout tous les enregistrements A et AAAA via dns_get_record() (et non gethostbyname(), qui ne renvoie que la première réponse) et rejette si une seule adresse résolue est privée ou réservée.

L’usage de la résolution de tous les enregistrements est délibéré et documenté dans le docblock de la classe comme une défense contre un hôte qui renvoie plusieurs enregistrements : une recherche limitée à un seul enregistrement pourrait choisir l’adresse publique, tandis que la connexion ultérieure en choisirait une privée. Cela correspond à l’OWASP SSRF Prevention Cheat Sheet, qui demande à une application de récupérer toutes les adresses IP derrière le nom de domaine (enregistrements A et AAAA) et d’appliquer la vérification d’adresse non publique à chacune d’elles.

validateWorkerUrl() renvoie l’ensemble d’IP vérifié. Le moteur de rendu appelle ensuite assertPinsStillValid() juste avant l’envoi. Cet appel résout à nouveau l’hôte et rejette si une nouvelle IP est apparue depuis la validation (Worker URL DNS answer changed since validation — possible DNS rebinding attack). Cela ferme la fenêtre entre le moment de la vérification et celui de l’utilisation, entre validation et connexion.

Lorsqu’un ensemble d’IP vérifié ou un ensemble d’épingles SPKI est présent et qu’une ResponseFactory PSR-17 a été fournie, le moteur de rendu utilise Transport\PinnedCurlTransport au lieu du client PSR-18 injecté. Le transport applique, au niveau du handle cURL :

  • DNS épingléCURLOPT_RESOLVE lie le couple hôte:port à l’ensemble d’IP vérifié, afin que libcurl n’effectue pas sa propre résolution au moment de la connexion. C’est ce qui fait que la vérification DNS en espace utilisateur lie réellement la connexion ; sans cela, libcurl pourrait résoudre une adresse différente.
  • Épinglage de clé publique TLSCURLOPT_PINNEDPUBLICKEY est défini à partir de l’ensemble d’épingles combiné. Cela suit la RFC 7469 §2.6 : une connexion épinglée est acceptée lorsque l’ensemble des empreintes SPKI présentées par le serveur présente une intersection avec l’ensemble d’épingles configuré, et l’échec de validation d’épingle est traité comme non récupérable. Les chaînes d’épingle sont normalisées de la forme sha256/<base64> vers la forme sha256//<base64> de cURL ; une épingle mal formée lève InvalidSpkiPinException.
  • Vérification TLS activéeCURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2.
  • Aucune redirection automatiqueCURLOPT_FOLLOWLOCATION => false, CURLOPT_MAXREDIRS => 0. Une réponse 3xx remonte à la couche de politique plutôt que d’être suivie par libcurl vers un hôte non vérifié. Le docblock de la classe indique qu’il s’agit d’un choix délibéré, afin que les redirections soient revalidées et non suivies en silence.
  • Délai maximal strictCURLOPT_TIMEOUT est défini à partir de renderTimeout (par défaut 30 secondes).

Une erreur cURL ou un corps de réponse qui n’est pas une chaîne lève CloudflareRenderException avec le numéro et le message d’erreur cURL.

La configuration expose pinnedPublicKeys et un champ distinct backupPublicKeys. La RFC 7469 §2.5 décrit une épingle de secours — l’empreinte d’une paire de clés secondaire, pas encore déployée et conservée hors ligne — comme le principal moyen de récupérer après un échec involontaire de validation d’épingle. Conserver au moins une épingle de secours afin qu’une rotation de certificat ne mette pas le point de terminaison hors service suit cette recommandation. Le champ distinct permet de valider une rotation indépendamment. En pratique :

  • Épingle le SPKI de la feuille ou d’un intermédiaire dont tu contrôles la rotation.
  • Configure toujours une épingle de secours pour le prochain certificat avant de faire la rotation.
  • Un ensemble d’épingles vide désactive l’épinglage ; ne l’utilise qu’avec une chaîne de certificats stable et connue. L’épinglage s’active par configuration.
  • La requête vers le Worker inclut Authorization: Bearer <apiToken>. apiToken est #[SensitiveParameter], donc il est expurgé des traces de pile. La vérification de joignabilité envoie le même en-tête bearer sur un HEAD HTTP.
  • Les clés d’accès R2 (accessKeyId, secretAccessKey) sont #[SensitiveParameter] et servent uniquement à dériver la clé de signature AWS Signature V4.
  • ApiKeyValidator compare les clés avec hash_equals() (en temps constant) et prend en charge le stockage de clés hachées en SHA-256 via validateHashed().
  • Les objets de configuration sont final readonly — une fois défini, un secret ne peut plus être modifié.
  • Charge les secrets depuis des variables d’environnement ou un gestionnaire de secrets. Ne les envoie jamais dans un commit. Le paquet suit le socle de sécurité plus large de NextPDF : PHPStan niveau 10, declare(strict_types=1) dans chaque fichier, pas de eval()/exec(), GitHub Actions épinglées au SHA.
  • Il n’énonce aucune limite de plateforme Cloudflare (temps CPU du Worker, mémoire, plafond de corps de requête ou nombre de sous-requêtes). Les seules limites de taille et de temps que cette documentation énonce sont celles que le paquet applique lui-même, listées ci-dessus et dans /integrations/cloudflare/configuration/. Pour les limites de plateforme, consulte la documentation officielle de Cloudflare et l’implémentation de ton propre Worker.
  • Il ne signe pas les PDF et ne formule aucune revendication de conformité de signature. Lorsque des signatures sont requises, effectue le rendu ici, puis signe avec le moteur. NextPDF Pro fournit uniquement la signature PAdES B-B ; les profils de validation à long terme sont une capacité Enterprise et sont hors du périmètre de ce pont.
  • Il ne certifie pas, ne garantit pas et ne rend pas le pipeline « inviolable ». Il met en œuvre les contrôles spécifiques, vérifiables dans le code source et décrits sur cette page, et rien au-delà.
SymptômePremière vérification
Worker URL must use HTTPSLe schéma du workerUrl configuré.
private or reserved IPLes enregistrements DNS du nom d’hôte du Worker ; un enregistrement se résout dans l’espace RFC 1918 / bouclage local / RFC 3927.
DNS answer changed since validationInstabilité DNS ou tentative de rebinding ; résous à nouveau et inspecte l’ensemble des enregistrements.
cURL transport errorLe chemin réseau, la chaîne TLS et — si des épingles sont définies — le SPKI du certificat servi est toujours dans l’ensemble d’épingles.
Les rendus échouent juste après une rotation de certificatUn ensemble d’épingles sans épingle de secours correspondante. Ajoute le nouveau SPKI comme secours avant de faire la rotation.
is not installed / no LocalRendererFactoryInterfaceLe repli est activé, mais aucune fabrique n’est câblée, ou nextpdf/artisan est absent.
Refus incohérents liés à la limitation de débit d’un nœud à l’autreLe limiteur en mémoire est par processus ; place-le derrière un magasin partagé.

Signale les vulnérabilités via les GitHub Security Advisories ou le contact de sécurité indiqué dans le SECURITY.md du dépôt. Ne dépose pas les problèmes de sécurité comme des issues GitHub publiques.

  • /integrations/cloudflare/overview/ — pourquoi le paquet est structuré autour de cette frontière.
  • /integrations/cloudflare/configuration/ — champs d’ensemble d’épingles et de limite.
  • /integrations/cloudflare/troubleshooting/ — correspondance complète entre échecs et exceptions.