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.
Modèle de menace
Section intitulée « Modèle de menace »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ôle | Comportement | Source de la limite |
|---|---|---|
| Plafond de taille | Rejette un HTML plus volumineux que maxHtmlSize | CloudflareRendererConfig, valeur par défaut 5000000 octets |
| Garde-fou contre les bombes de décompression Base64 | Estime la taille décodée de chaque URI data:…;base64,… ; rejette dès que la limite est atteinte ou dépassée | MAX_DATA_URI_BYTES = 13631488 |
| Interdiction du meta-refresh | Rejette tout <meta http-equiv="refresh">, sans tenir compte de la casse | expression 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.
Contrôles de destination (SSRF et rebinding DNS)
Section intitulée « Contrôles de destination (SSRF et rebinding DNS) »CloudflareSecurityPolicy::validateWorkerUrl() :
- Rejette une URL qui ne peut pas être analysée ou à laquelle il manque un scheme/host (
Invalid Worker URL). - Rejette tout schéma non-HTTPS (
Worker URL must use HTTPS). - 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_RANGEde 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 de192.168.x,127.0.0.1et169.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. - Pour un nom d’hôte, résout tous les enregistrements A et AAAA via
dns_get_record()(et nongethostbyname(), 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.
Contrôles de transport (PinnedCurlTransport)
Section intitulée « Contrôles de transport (PinnedCurlTransport) »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_RESOLVElie 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 TLS —
CURLOPT_PINNEDPUBLICKEYest 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 formesha256/<base64>vers la formesha256//<base64>de cURL ; une épingle mal formée lèveInvalidSpkiPinException. - Vérification TLS activée —
CURLOPT_SSL_VERIFYPEER => true,CURLOPT_SSL_VERIFYHOST => 2. - Aucune redirection automatique —
CURLOPT_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 strict —
CURLOPT_TIMEOUTest défini à partir derenderTimeout(par défaut30secondes).
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.
Guide opérationnel de l’épinglage
Section intitulée « Guide opérationnel de l’épinglage »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.
Authentification et gestion des secrets
Section intitulée « Authentification et gestion des secrets »- La requête vers le Worker inclut
Authorization: Bearer <apiToken>.apiTokenest#[SensitiveParameter], donc il est expurgé des traces de pile. La vérification de joignabilité envoie le même en-tête bearer sur unHEADHTTP. - Les clés d’accès R2 (
accessKeyId,secretAccessKey) sont#[SensitiveParameter]et servent uniquement à dériver la clé de signature AWS Signature V4. ApiKeyValidatorcompare les clés avechash_equals()(en temps constant) et prend en charge le stockage de clés hachées en SHA-256 viavalidateHashed().- 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 deeval()/exec(), GitHub Actions épinglées au SHA.
Ce que ce paquet n’affirme pas
Section intitulée « Ce que ce paquet n’affirme pas »- 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à.
Guide d’exploitation
Section intitulée « Guide d’exploitation »| Symptôme | Première vérification |
|---|---|
Worker URL must use HTTPS | Le schéma du workerUrl configuré. |
private or reserved IP | Les 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 validation | Instabilité DNS ou tentative de rebinding ; résous à nouveau et inspecte l’ensemble des enregistrements. |
cURL transport error | Le 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 certificat | Un ensemble d’épingles sans épingle de secours correspondante. Ajoute le nouveau SPKI comme secours avant de faire la rotation. |
is not installed / no LocalRendererFactoryInterface | Le 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’autre | Le limiteur en mémoire est par processus ; place-le derrière un magasin partagé. |
Signalement d’incident
Section intitulée « Signalement d’incident »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.
Voir aussi
Section intitulée « Voir aussi »- /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.