- En tant que protocole de proxy transmettant les requêtes par socket à des backends de longue durée de vie, il peut être adopté sans presque modifier la structure existante des handlers HTTP
- Le reverse proxy HTTP/1.1 se prête facilement à des divergences d’interprétation des frontières de message selon les implémentations, ce qui continue de provoquer de graves problèmes de sécurité comme le desync et le request smuggling
- FastCGI fournit un framing de message clair depuis 1996 et sépare structurellement les en-têtes du client des informations de confiance ajoutées par le proxy
- Dans Go,
net/http/fcgirenseigneREMOTE_ADDRdansRequest.RemoteAddret reflète aussi l’usage de HTTPS dansRequest.TLS, ce qui permet de gérer la transmission des informations de confiance sans middleware distinct - Il a des limites, comme l’absence de prise en charge de WebSockets, un écosystème d’outils faible et un débit plus faible sur certaines charges, mais si WebSockets n’est pas nécessaire et que les performances suffisent, cela reste un choix pratique
Place de FastCGI et mode d’adoption
- FastCGI ne sert pas seulement à exécuter un processus par fichier : il peut aussi être utilisé comme protocole proxy-backend envoyant les requêtes à un démon de longue durée de vie via TCP ou un socket UNIX
- En Go, il suffit en gros d’importer le package
net/http/fcgiet de remplacerhttp.Serveparfcgi.Serve- Les handlers existants continuent d’utiliser
http.ResponseWriterethttp.Request - Le reste de la structure de l’application reste inchangé
- Les handlers existants continuent d’utiliser
- Les principaux proxies comme Apache, Caddy, nginx et HAProxy prennent en charge les backends FastCGI, avec une configuration plutôt simple
Problèmes de parsing quand HTTP est utilisé comme protocole backend
- Le reverse proxy HTTP ressemble à un champ de mines en matière de sécurité, et des problèmes continuent d’apparaître, comme la vulnérabilité de desync du proxy média de Discord, qui permettait d’entrevoir des pièces jointes privées
- HTTP/1.1 paraît être un protocole texte simple, mais il existe trop de façons de représenter un même message, avec de nombreuses exceptions, si bien que l’interprétation varie facilement selon les implémentations
- Le plus gros problème est l’absence de framing explicite dans les messages HTTP
- La fin d’un message est décrite de plusieurs façons par le message lui-même
- Selon l’implémentation, le point de fin du message et le début du suivant peuvent être interprétés différemment
- Ces divergences servent de base aux HTTP desync attacks ou au request smuggling, créant de graves problèmes de sécurité quand le reverse proxy et le backend comprennent différemment les frontières des messages
- Continuer à corriger les différences entre parseurs a peu de chances d’être une solution de fond
- James Kettle continue de découvrir de nouveaux types d’attaques
- Après avoir trouvé d’autres cas l’an dernier, il est même allé jusqu’à dire "HTTP/1.1 must die"
Gestion des frontières de message dans FastCGI et HTTP/2
- HTTP/2, s’il est utilisé de façon cohérente entre le proxy et le backend, peut résoudre les problèmes de desync en clarifiant les frontières des messages
- FastCGI fournit cette séparation claire des frontières depuis 1996, avec un protocole plus simple
- nginx prend en charge les backends FastCGI depuis sa première version, mais la prise en charge des backends HTTP/2 n’a été ajoutée qu’à la fin de 2025
- Chez Apache, la prise en charge des backends HTTP/2 reste encore "experimental"
Problème des en-têtes non fiables et mode de séparation de FastCGI
- Le problème ne se limite pas au desync : HTTP manque aussi d’un moyen robuste de transporter des données que le proxy doit transmettre de manière fiable, comme la véritable IP du client, le nom d’utilisateur authentifié par le proxy, ou les informations de certificat client en mTLS
- En pratique, ces informations sont placées dans des en-têtes HTTP, mais il n’existe aucune séparation structurelle entre les données de confiance ajoutées par le proxy et les en-têtes non fiables envoyés par le client
- Des en-têtes comme
X-Real-IPsont souvent utilisés pour transmettre la véritable IP du client, mais pour être sûr, le proxy doit d’abord supprimer complètement tous les en-têtes existants correspondants, y compris leurs variantes de casse, puis les réinsérer - C’est un terrain extrêmement dangereux, avec de nombreux chemins possibles menant le backend à faire confiance à des données injectées par un attaquant
- Le proxy doit supprimer non seulement
X-Real-IP, mais tout en-tête utilisé à cette fin - Par exemple, le middleware Chi détermine la véritable IP du client en vérifiant d’abord
True-Client-IP, puis seulement en son absenceX-Real-IP- Même si le proxy traite correctement
X-Real-IP, un attaquant peut poser problème en envoyantTrue-Client-IP
- Même si le proxy traite correctement
- FastCGI sépare les en-têtes du client et les informations ajoutées par le proxy selon un principe de séparation de domaine
- Les deux sont transmis comme des listes de paramètres clé/valeur, mais les noms d’en-têtes HTTP reçoivent le préfixe
HTTP_ - Un en-tête envoyé par le client ne peut donc pas être interprété structurellement comme une donnée de confiance du proxy
- Les deux sont transmis comme des listes de paramètres clé/valeur, mais les noms d’en-têtes HTTP reçoivent le préfixe
Gestion des informations de confiance FastCGI dans Go
- FastCGI définit des paramètres standard comme
REMOTE_ADDRpour transmettre la véritable IP du client - Dans Go,
net/http/fcgicopie automatiquement cette valeur danshttp.Request.RemoteAddr, ce qui fonctionne sans middleware supplémentaire - Le proxy peut aussi transmettre, via des paramètres non standard, des informations comme l’usage de HTTPS, la suite de chiffrement TLS négociée ou le certificat client
- Go définit automatiquement le champ
TLSdeRequestà une valeur non nulle quand la requête a utilisé HTTPS- Même vide, cela reste utile pour vérifier qu’un accès HTTPS est imposé
fcgi.ProcessEnvpermet d’accéder à l’ensemble complet des paramètres de confiance envoyés par le proxy
Pourquoi l’adoption reste lente et quelles sont les limites réelles
- Si FastCGI est meilleur, pourquoi n’est-il pas plus largement utilisé ? L’article estime que le caractère daté du nom et le manque de conscience des problèmes de sécurité du reverse proxy HTTP jouent tous les deux un rôle
- Watchfire traitait déjà des attaques de desync en 2005 et avertissait qu’elles seraient difficiles à résoudre, mais ces attaques n’ont pas reçu une vraie attention pendant plus de dix ans
- FastCGI reste utilisable en pratique aujourd’hui, et SSLMate l’utilise en production depuis plus de 10 ans
- Cela reste néanmoins une ancienne technologie, avec ses points faibles
- Elle n’a pas été mise à jour pour prendre en charge WebSockets
- Son écosystème d’outils est limité
- Par exemple,
curlprend en charge FTP, Gopher et SMTP, mais ne peut pas envoyer de requêtes FastCGI
- Lors de benchmarks d’un serveur FastCGI en Go derrière plusieurs reverse proxies, certaines charges présentaient un débit inférieur à HTTP/1.1 ou HTTP/2
- L’auteur y voit moins une limite intrinsèque du protocole qu’un manque d’optimisation du chemin de code FastCGI par rapport à HTTP
Jugement final
- Si WebSockets n’est pas nécessaire et que les performances actuelles suffisent, FastCGI reste une option tout à fait valable
- Même si un goulot d’étranglement apparaît, il vaut mieux ajouter du matériel que d’accepter la complexité et le cauchemar sécuritaire du reverse proxy HTTP
2 commentaires
Le commentaire de Twisted sur FastCGI trouvé dans les commentaires de Lobsters est assez marquant : https://web.archive.org/web/20160723091923/…
Réactions sur Hacker News
Je suis d’accord avec l’idée générale. Pour cet usage, FastCGI me semble meilleur que HTTP
J’aimerais aussi faire connaître un protocole appelé WAS (Web Application Socket). Il y a 16 ans, au travail, j’avais estimé que même FastCGI n’était pas assez satisfaisant, alors je l’ai conçu moi-même
Au lieu d’un framing sur socket principal, il utilise un socket de contrôle et deux pipes bruts pour les corps de requête/réponse, et l’application WAS comme le serveur web peuvent utiliser
splice()sur les pipesIl n’y a pas besoin de framing, l’annulation des requêtes est possible, et j’ai fait en sorte qu’on puisse toujours récupérer les trois descripteurs de fichier
Nous l’utilisons depuis des années pour des applications internes et des environnements d’hébergement web, et j’ai aussi écrit moi-même une PHP SAPI. Pas mal de sites web tournent en interne sur WAS
Tout est open source
library: https://github.com/CM4all/libwas
documentation: https://libwas.readthedocs.io/en/latest/
non-blocking library: https://github.com/CM4all/libcommon/tree/master/src/was/asyn...
our web server: https://github.com/CM4all/beng-proxy
WebDAV: https://github.com/CM4all/davos
PHP fork with WAS SAPI: https://github.com/CM4all/php-src
HTTP sert à transporter les données entre les deux extrémités, par exemple le navigateur et le serveur, tandis que FastCGI sert à traiter ces données entre le serveur et l’application
Je viens de parcourir l’article et j’ai l’impression que l’auteur écrit d’une manière qui prête à confusion, comme si les deux étaient interchangeables. En réalité, pas du tout
À titre indicatif, j’utilise moi aussi fcgi depuis 10 ans pour des services web destinés aux clients
Cet article est d’autant plus intéressant qu’il omet beaucoup de choses
J’ai fondé une startup Web2.0 à l’époque où le débat FastCGI vs. SCGI vs. HTTP battait son plein, et j’ai monté moi-même la stack frontend ; au final, si HTTP a gagné, c’est pour une raison simple : la simplicité
Comme il fallait de toute façon déjà gérer HTTP au niveau de la gateway, l’utiliser tel quel évitait d’ajouter un autre protocole à la stack, ce qui rendait très facile l’ajout de plusieurs couches de reverse proxy ou la séparation des préoccupations transverses — authentification, sessions, terminaison SSL, filtrage DDoS — entre des serveurs aux rôles distincts
En développement, on se connectait directement au serveur applicatif en HTTP, et en production, le reverse proxy prenait en charge le SSL, l’authentification et la détection des abus, ce qui permettait de réutiliser exactement le même serveur applicatif
À l’époque, nginx était aussi bien plus rapide et plus stable que la plupart des modules FastCGI/SCGI. Au départ, notre architecture était
HTTP -> Lighttpd -> FastCGI -> Django, mais utiliser simplement nginx était nettement plus rapideL’usage de HTTP fonctionnait comme une version web du principe end-to-end. L’idée, c’est que le réseau et les protocoles devraient être indépendants du contenu transporté, et que la logique applicative devrait se trouver aux extrémités, pas dans les nœuds réseau qui filtrent ou redirigent
Cela dit, le point essentiel soulevé par l’article est que, du point de vue de la sécurité, il vaut souvent mieux suivre le principe du moindre privilège. Il faut ne laisser passer via allowlist que les communications prévues, pour éviter de contribuer sans le vouloir à une compromission ailleurs
Au fond, il y a une tension entre les deux. E2E apporte de la flexibilité, mais cette flexibilité ouvre aussi plus de possibilités d’abus, tandis que le PoLP apporte de la sécurité, mais ne permet de faire que ce qui a été prévu, ce qui rend l’adaptation à de nouveaux besoins plus difficile
[1] https://en.wikipedia.org/wiki/End-to-end_principle
[2] https://en.wikipedia.org/wiki/Principle_of_least_privilege
Si une gateway intermédiaire multiplexe plusieurs requêtes HTTP sur un autre canal HTTP unique, que ce canal va directement jusqu’au service à l’écoute, et qu’il n’est démultiplexé qu’après le socket applicatif, alors cela viole fondamentalement la logique end-to-end de plusieurs façons
Cette analogie n’a encore un peu de sens que si la symétrie d’une correspondance 1:1 entre connexions est préservée
À mon avis, les vulnérabilités des reverse proxies découlent toutes directement d’une rupture du end-to-end
Si l’analogie était correcte, alors l’acheminement SMTP via plusieurs MX devrait aussi être end-to-end, alors qu’en pratique ce n’est pas le cas et qu’on y voit beaucoup de problèmes similaires à ceux des reverse proxies, par exemple la désynchronisation des frontières de message
Je comprends l’intention de faire correspondre les requêtes HTTP à des messages, mais cela s’effondre vite à cause des sémantiques réelles de TCP et HTTP, ainsi que de tous les détails protocolaires
Le principe end-to-end n’autorise pas un traitement approximatif de la sémantique. Il impose une discipline très stricte sur la gestion d’état et les frontières de la couche de transport. Quelque chose qui ressemble vaguement à du end-to-end n’est pas du end-to-end
Par exemple, il n’y avait pas de multiplexing avant HTTP 2.0, donc utiliser HTTP tel quel entre reverse proxy et backend est très inefficace
Il y a aussi des problèmes de sécurité. Deux parseurs peuvent ne même pas s’accorder sur l’endroit exact où se termine une frontière de requête
Google, depuis longtemps déjà, encapsule HTTP entre ses serveurs web frontaux et ses applications dans son protocole interne Stubby
C’est bien plus rapide que le wire protocol HTTP et plus riche en fonctionnalités. C’est excessif pour la plupart des entreprises, mais à grande échelle le coût de créer un autre wire protocol et tout l’outillage autour est largement justifié
httpd aussi a fini par aller dans le sens d’une configuration plus compliquée, et je l’ai abandonné au moment où il a brusquement changé de format de configuration
J’aurais pu m’y adapter, mais j’ai préféré migrer vers lighttpd, puis ruby a automatisé la génération de la configuration, donc techniquement je pourrais revenir à httpd
Malgré cela, je n’ai aucune envie d’y retourner. Les développeurs de serveurs web devraient être prudents avant de forcer les utilisateurs à se conformer à un nouveau format
Si l’on va changer le format de configuration pour une décision aussi simple, il faudrait au minimum proposer quelque chose comme une configuration yaml en option, au lieu d’imposer soudainement un nouveau style de directives à base de if-clause
Maintenant que les WHATWG streams sont largement répandus dans les navigateurs, il est assez facile d’implémenter une sorte de pseudo-WebSocket au-dessus d’une requête HTTP longue durée
Il suffit d’envoyer un flux d’octets et de préfixer chaque message par un en-tête ; dans beaucoup de cas, une simple longueur suffit
Il y a aussi des avantages. Pas besoin d’un chemin spécial supplémentaire dans la couche serveur comme avec WebSocket, on peut utiliser le backpressure, on profite gratuitement des améliorations de HTTP/2 et HTTP/3, et le surcoût de framing est plus faible
En revanche, AFAIK, il n’est toujours pas possible de continuer à streamer le corps de la requête tout en recevant simultanément la réponse, donc pour un vrai streaming bidirectionnel il faut deux requêtes
J’ai redécouvert le vieux plain CGI, et c’est excellent pour permettre aux utilisateurs de faire du vibe code sur des pages personnalisées dans notre plateforme [1]
Les fonctionnalités fournies de base sont une task list et un data viewer, mais les utilisateurs veulent souvent des personnalisations beaucoup plus fines, comme une vue Kanban ou des tableaux de bord sur mesure avec filtres et graphiques
Cette box intègre un coding agent, ce qui nous évite de construire un report builder traditionnel et permet aux utilisateurs de coder directement ce qu’ils veulent
La stdlib Go offre un bon support côté serveur comme en espace utilisateur, et si le coding agent crée
page-name/main.gopour communiquer en CGI, le serveur délègue simplement les requêtes à cet endroitLe volume de données et les pages vues restent tous à une échelle individuelle, donc des optimisations comme FastCGI ne sont pas vraiment nécessaires
À l’ère des agents, les vieilles technologies redeviennent nouvelles
L’implémentation du serveur CGI de Go n’expose pas
$HTTP_PROXY, donc ce point est sûr, mais malgré cela je n’aime toujours pas la manière dont CGI utilise les variables d’environnementCôté reverse proxy, j’ai presque toujours eu affaire à des tâches simples, donc les fonctionnalités intégrées de Nginx suffisaient largement
Même quand il fallait quelque chose de plus complexe, je ne pense pas que l’idée d’utiliser FastCGI me serait venue
Il y a environ 10 ans, j’ai un peu utilisé FastCGI pour faire tourner une partie de code C++ sur le web, mais depuis, pratiquement plus jamais
Il suffit d’intégrer directement un serveur HTTP dans l’application et de faire ce qu’il faut, sans gateway
Dans l’écosystème Red Hat, la configuration PHP/Apache distribuée utilise FPM (FastCGI Process Manager)
Je ne sais pas si FastCGI est utilisé ailleurs dans les distributions RHEL
$ rpm -qi php-fpm | grep ^SummarySummary : PHP FastCGI Process ManagerIl est inclus dans le paquet
httpd-corede Fedora. Pour RHEL, je ne sais pas bien : https://packages.fedoraproject.org/pkgs/httpd/httpd-core/fed...Il y a aussi le uwsgi protocol
En pratique, c’est un peu comme une sorte de RPC pour à peu près tout
FCGI est aussi un système d’orchestration
Quand la charge monte, il lance plus de tâches serveur, puis les réduit quand la charge baisse, et si une tâche meurt il en démarre une nouvelle copie
C’est une sorte de Kubernetes sur une seule machine
Sur le papier, ça paraît séduisant, mais en pratique on se retrouvait souvent avec un système qui tournait bien à faible charge puis épuisait sa mémoire à forte charge en créant davantage de workers
C’est pourquoi un nombre statique de workers donnait généralement de meilleurs résultats
En revanche, le crash recovery est utile si on en a besoin
Prenons juste un instant pour admirer l’absurdité des en-têtes HTTP
Si on n’utilise
X-Real-IPqu’en l’absence deTrue-Client-IP, alors même si le proxy renseigne correctementX-Real-IP, un attaquant peut simplement envoyer un en-têteTrue-Client-IPet ça suffit à vous piégerIl y a
X-Forwarded-For,X-Real-IP, et des en-têtes personnalisés différents selon les CDN ; certains sont des listes séparées par des virgules, avec souvent en plus l’IP de notre propre LB ajoutée inutilement à la finJe comprends pourquoi c’est ainsi, mais ce n’est absolument pas utile
En plus, tous ces en-têtes peuvent être injectés par un user-agent malveillant. On dirait que personne n’a réussi à se mettre d’accord sur la manière dont des serveurs de confiance devraient transmettre des informations importantes dans un pipeline
Ce chaos va d’ailleurs très bien avec l’absurdité de l’en-tête User-Agent
Là, Apple est allé encore plus loin en décidant, au nom de la vie privée, d’envoyer des informations complètement fausses — par exemple de fausses versions d’OS
Il y a beaucoup de vrai dans cette affirmation, mais FastCGI perd de l’information parce qu’il suit CGI/1.1 sur des points comme
PATH_INFOLe décodage d’URL y est imposé, ce qui rend impossible de représenter un slash encodé comme
%2FSelon l’implémentation,
//dans le chemin peut aussi être réduit à/, même si c’est également un problème présent dans plusieurs implémentations HTTPEn termes d’expressivité, c’est inférieur à HTTP, et l’importance de cette différence dépend de l’application
Personnellement, je préfère une gestion exacte des URL