- Als Proxy-Protokoll, das Anfragen per Socket an langlaufende Backends weiterreicht, lässt es sich einsetzen, ohne die bestehende HTTP-Handler-Struktur wesentlich zu verändern
- HTTP/1.1-Reverse-Proxying führt leicht zu abweichenden Interpretationen von Nachrichtenbegrenzungen zwischen Implementierungen und kann dadurch weiterhin schwere Sicherheitsprobleme wie Desync und Request Smuggling verursachen
- FastCGI bietet seit 1996 eine klare Nachrichten-Framierung und trennt Client-Header strukturell von den vom Proxy hinzugefügten vertrauenswürdigen Informationen
- Gos
net/http/fcgifülltREMOTE_ADDRinRequest.RemoteAddrein und spiegelt den HTTPS-Status auch inRequest.TLSwider, sodass die Weitergabe von Vertrauensinformationen ohne zusätzliche Middleware verarbeitet werden kann - Es gibt Einschränkungen wie fehlende WebSocket-Unterstützung, ein schwaches Tooling-Ökosystem und bei manchen Workloads geringeren Durchsatz, doch wenn keine WebSockets nötig sind und die Leistung ausreicht, bleibt es eine praktische Option
Stellung und Einsatzweise von FastCGI
- FastCGI wird nicht nur für das Modell verwendet, bei dem pro Datei ein Prozess gestartet wird, sondern kann auch als Proxy-Backend-Protokoll dienen, das Anfragen über TCP- oder UNIX-Sockets an langlaufende Daemons sendet
- In Go genügt es, das Paket
net/http/fcgizu importieren undhttp.Servedurchfcgi.Servezu ersetzen- Bestehende Handler verwenden weiterhin
http.ResponseWriterundhttp.Request - Auch die übrige Struktur der Anwendung bleibt unverändert
- Bestehende Handler verwenden weiterhin
- Große Proxies wie Apache, Caddy, nginx und HAProxy unterstützen FastCGI-Backends, und die Konfiguration ist vergleichsweise einfach
Parsing-Probleme bei HTTP als Backend-Protokoll
- HTTP-Reverse-Proxying kommt einem Sicherheitsminenfeld nahe, und Probleme wie die Desync-Schwachstelle im Medien-Proxy von Discord, durch die sich private Anhänge ausspähen ließen, tauchen weiter auf
- HTTP/1.1 wirkt oberflächlich wie ein simples textbasiertes Protokoll, erlaubt aber zu viele verschiedene Darstellungen derselben Nachricht und zu viele Sonderfälle, sodass Implementierungen sie leicht unterschiedlich interpretieren
- Das größte Problem ist, dass HTTP-Nachrichten keine explizite Framierung haben
- Das Ende einer Nachricht wird von der Nachricht selbst auf mehrere Arten beschrieben
- Verschiedene Implementierungen können unterschiedlich auslegen, wo eine Nachricht endet und die nächste beginnt
- Solche Abweichungen bilden die Grundlage für HTTP-Desync-Angriffe beziehungsweise Request Smuggling und führen zu schweren Sicherheitsproblemen, weil Reverse Proxy und Backend die Nachrichtengrenzen unterschiedlich verstehen
- Parser-Unterschiede immer weiter zu patchen, ist kaum eine grundlegende Lösung
- James Kettle entdeckt fortlaufend neue Varianten
- Nachdem er im vergangenen Jahr weitere Fälle gefunden hatte, verwendete er sogar die Formulierung "HTTP/1.1 must die"
Umgang mit Nachrichtengrenzen in FastCGI und HTTP/2
- HTTP/2 kann Desync-Probleme lösen, wenn es zwischen Proxy und Backend konsistent eingesetzt wird, weil Nachrichtengrenzen eindeutig sind
- FastCGI bietet diese klare Trennung der Grenzen bereits seit 1996 in einem einfacheren Protokoll
- nginx unterstützt FastCGI-Backends seit der ersten Veröffentlichung, aber HTTP/2-Backends wurden erst Ende 2025 hinzugefügt
- Die HTTP/2-Backend-Unterstützung von Apache befindet sich weiterhin im Status "experimental"
Problem mit nicht vertrauenswürdigen Headern und FastCGIs Trennungsmodell
- Nicht nur Desync ist ein Problem: HTTP bietet auch keine robuste Möglichkeit, Daten zu transportieren, die ein Proxy vertrauenswürdig weitergeben muss, etwa die tatsächliche Client-IP, den vom Proxy verarbeiteten authentifizierten Benutzernamen oder Informationen zum Client-Zertifikat bei mTLS
- In der Praxis landen solche Informationen in HTTP-Headern, aber zwischen den vom Proxy hinzugefügten Vertrauensdaten und den vom Client gesendeten nicht vertrauenswürdigen Headern gibt es keine strukturelle Trennung
- Header wie
X-Real-IPwerden oft zur Weitergabe der echten Client-IP genutzt, sind aber nur dann sicher, wenn der Proxy alle vorhandenen entsprechenden Header vollständig entfernt und dann neu setzt, einschließlich Varianten der Groß- und Kleinschreibung - Dieser Ansatz ist äußerst riskant, und es gibt viele Wege, auf denen das Backend Daten vertrauen kann, die ein Angreifer eingeschleust hat
- Der Proxy muss nicht nur
X-Real-IP, sondern jeden Header für diesen Zweck löschen - So prüft beispielsweise die Chi-Middleware zur Ermittlung der tatsächlichen Client-IP zuerst
True-Client-IPund verwendet nur dannX-Real-IP, wenn ersterer fehlt- Selbst wenn der Proxy
X-Real-IPkorrekt verarbeitet, kann ein Angreifer Probleme verursachen, indem erTrue-Client-IPmitsendet
- Selbst wenn der Proxy
- FastCGI trennt Client-Header und vom Proxy hinzugefügte Informationen per Domain Separation
- Beide werden zwar als Listen von Schlüssel/Wert-Parametern übertragen, aber HTTP-Headernamen erhalten das Präfix
HTTP_ - Dadurch kann ein vom Client gesendeter Header nicht strukturell als Vertrauensdatum des Proxys interpretiert werden
- Beide werden zwar als Listen von Schlüssel/Wert-Parametern übertragen, aber HTTP-Headernamen erhalten das Präfix
Verarbeitung von Vertrauensinformationen mit FastCGI in Go
- FastCGI definiert Standardparameter wie
REMOTE_ADDR, um die echte Client-IP zu übermitteln - Gos
net/http/fcgiübernimmt diesen Wert automatisch inhttp.Request.RemoteAddr, sodass keine zusätzliche Middleware nötig ist - Der Proxy kann auch Informationen wie die Nutzung von HTTPS, die ausgehandelte TLS Cipher Suite oder das Client-Zertifikat über nicht standardisierte Parameter weitergeben
- In Go setzt das System bei HTTPS-Anfragen automatisch das Feld
TLSinRequestauf einen nicht-nil-Wert- Selbst wenn es leer ist, ist das nützlich, um zu prüfen, ob HTTPS erzwungen wurde
- Über
fcgi.ProcessEnvlässt sich auf den vollständigen Satz der vom Proxy gesendeten Vertrauensparameter zugreifen
Warum die Verbreitung stockt und welche praktischen Grenzen es gibt
- Wenn FastCGI besser ist, warum wird es dann nicht breiter eingesetzt? Hier scheinen das altmodisch klingende Name und ein mangelndes Bewusstsein für die Sicherheitsprobleme von HTTP-Reverse-Proxies zusammenzuwirken
- Watchfire behandelte Desync-Angriffe bereits 2005 und warnte auch davor, dass sie schwer zu beheben seien, doch über mehr als ein Jahrzehnt hinweg erhielten solche Angriffe kaum angemessene Aufmerksamkeit
- FastCGI ist auch heute noch produktiv einsetzbar, und bei SSLMate wird es seit über 10 Jahren in Produktion verwendet
- Dennoch hat diese alte Technik auch Schwächen
- Sie wurde nicht für WebSocket-Unterstützung weiterentwickelt
- Das Tooling-Ökosystem ist schwach
- So unterstützt curl zwar FTP, Gopher und SMTP, kann aber keine FastCGI-Anfragen senden
- Bei Benchmarks eines Go-FastCGI-Servers hinter mehreren Reverse Proxies zeigten einige Workloads geringeren Durchsatz als mit HTTP/1.1 oder HTTP/2
- Das wird eher als Folge davon gesehen, dass der FastCGI-Codepfad nicht so stark optimiert ist wie HTTP, und nicht als grundsätzliche Grenze des Protokolls
Schlussfolgerung
- Wenn keine WebSockets benötigt werden und die aktuelle Leistung ausreicht, ist FastCGI weiterhin eine brauchbare Option
- Selbst wenn ein Flaschenhals entsteht, erscheint es sinnvoller, zusätzliche Hardware einzusetzen, als die Komplexität und den Sicherheitsalbtraum von HTTP-Reverse-Proxying in Kauf zu nehmen
2 Kommentare
Der FastCGI-Kommentar von Twisted, den ich in den Lobsters-Kommentaren gefunden habe, ist wirklich beeindruckend: https://web.archive.org/web/20160723091923/…
Hacker-News-Kommentare
Ich stimme der Stoßrichtung des Artikels zu. Für diesen Einsatzzweck halte ich FastCGI für besser als HTTP
Ich möchte auch das Protokoll WAS (Web Application Socket) bekannt machen. Ich habe es vor 16 Jahren bei der Arbeit selbst entworfen, weil ich fand, dass selbst FastCGI nicht gut genug war
Statt Main-Socket-Framing verwendet es einen Control-Socket und zwei Pipes für rohe Request-/Response-Bodies, und sowohl WAS-Apps als auch Webserver können auf den Pipes
splice()nutzenEs braucht kein Framing, Request-Abbruch ist möglich, und die drei File Descriptors lassen sich jederzeit wiederherstellen
Ich nutze es seit Jahren für interne Anwendungen und in Webhosting-Umgebungen und habe auch selbst eine PHP SAPI geschrieben. Ziemlich viele Websites laufen intern auf WAS
Alles ist 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 dient der Übertragung von Daten zwischen Endpunkten wie Browser und Server, FastCGI dagegen der Verarbeitung dieser Daten zwischen Server und Anwendung
Ich habe den Artikel gerade überflogen, und der Autor scheint verwirrend so zu schreiben, als seien beide gegenseitig austauschbar. In Wirklichkeit sind sie das überhaupt nicht
Zur Einordnung: Ich selbst nutze fcgi seit 10 Jahren für Web-Kundendienste
Dieser Artikel ist gerade deshalb interessant, weil so viel fehlt
Ich habe damals während der Hochphase der Debatte FastCGI vs. SCGI vs. HTTP ein Web2.0-Startup gegründet und den Frontend-Stack selbst aufgebaut; am Ende hat HTTP aus Gründen der Einfachheit gewonnen
Wenn man einfach das HTTP weiterverwendet, das am Gateway ohnehin verarbeitet werden muss, braucht man kein weiteres Protokoll im Stack, und dadurch wurde es sehr einfach, mehrere Reverse-Proxy-Stufen einzuziehen oder Querschnittsthemen wie Authentifizierung, Sessions, SSL-Terminierung und DDoS-Filterung auf Server mit jeweils eigener Rolle aufzuteilen
In der Entwicklungsumgebung konnte man direkt per HTTP an den App-Server gehen, und in Produktion konnte derselbe App-Server unverändert weiterverwendet werden, während Reverse Proxies SSL, Authentifizierung und Missbrauchserkennung übernahmen
Damals war auch wichtig, dass nginx deutlich schneller und stabiler war als die meisten FastCGI-/SCGI-Module. Wir hatten anfangs
HTTP -> Lighttpd -> FastCGI -> Django, aber einfach nginx zu verwenden war viel schnellerDie Verwendung von HTTP funktionierte wie eine Web-Variante des End-to-End Principle. Die Idee ist, dass Netzwerke und Protokolle vom transportierten Inhalt unabhängig sein sollten und dass die Anwendungslogik an den Endpunkten sitzen sollte, nicht in Netzknoten, die filtern oder umleiten
Der zentrale Punkt des Artikels ist allerdings, dass es aus Sicherheitssicht oft besser ist, dem Prinzip der geringsten Rechte zu folgen. Man sollte nur die erwartete Kommunikation per Allowlist zulassen, damit man nicht versehentlich zu einer Kompromittierung an anderer Stelle beiträgt
Am Ende gibt es zwischen beiden einen Spannungsbogen. E2E bringt Flexibilität, aber diese Flexibilität schafft auch mehr Spielraum für Missbrauch; PoLP bringt Sicherheit, erlaubt aber nur das, was man entworfen hat, und erschwert dadurch die Anpassung an neue Anforderungen
[1] https://en.wikipedia.org/wiki/End-to-end_principle
[2] https://en.wikipedia.org/wiki/Principle_of_least_privilege
Wenn ein zwischengeschaltetes Gateway mehrere HTTP-Requests in einen anderen einzelnen HTTP-Kanal multiplexed, dieser Kanal direkt bis zum Listening Service durchläuft und nicht vor dem Application Socket wieder demultiplexed wird, dann verletzt das die End-to-End-Logik auf fundamentale Weise in mehrfacher Hinsicht
Die Analogie trägt höchstens dann einigermaßen, wenn eine 1:1-Verbindungssymmetrie erhalten bleibt
Ich würde sagen, dass Reverse-Proxy-Schwachstellen allesamt direkt daraus entstehen, dass End-to-End verletzt wurde
Wenn die Analogie stimmen würde, müsste auch SMTP-Übertragung über mehrere MX hinweg End-to-End sein, ist sie aber nicht, und dort treten viele ähnliche Probleme wie bei Reverse Proxies auf, etwa Message-Boundary-Desync
Ich verstehe die Absicht, HTTP-Requests Nachrichten zuzuordnen, aber an den realen TCP-/HTTP-Semantiken und all den Protokolldetails bricht das sehr schnell auseinander
Das End-to-End-Prinzip erlaubt keinen schlampigen Umgang mit Semantik. Es verlangt sehr strikte Disziplin bei Zustandsverwaltung und Grenzen der Transportebene. Irgendwie end-to-end-artig ist nicht End-to-End
Multiplexing gab es zum Beispiel erst mit HTTP 2.0, daher ist es verschwenderisch, HTTP unverändert für die Kommunikation zwischen Reverse Proxy und Backend zu verwenden
Es gibt auch Sicherheitsprobleme. Unterschiedliche Parser können sogar unterschiedlich interpretieren, wo Request-Grenzen enden
Google verwendet schon seit Langem zwischen Front-Webserver und Anwendung nicht direkt HTTP, sondern kapselt es in das eigene Stubby-Protokoll
Das ist viel schneller und funktionsreicher als das HTTP-Wire-Protocol. Für die meisten Unternehmen wäre das übertrieben, aber ab einer gewissen Größenordnung lohnt es sich völlig, die Kosten für ein eigenes Wire Protocol samt zugehörigem Tooling zu tragen
Auch httpd ist irgendwann in eine Richtung gegangen, die Konfiguration schwieriger macht, und ich habe es an dem Punkt aufgegeben, als das Konfigurationsformat plötzlich geändert wurde
Ich hätte mich wohl anpassen können, bin stattdessen aber zu lighttpd gewechselt, und später hat Ruby die Erzeugung der Konfiguration automatisiert, sodass ich technisch gesehen wieder zu httpd zurückkönnte
Trotzdem möchte ich nicht zurück. Webserver-Entwickler sollten sehr vorsichtig damit sein, Nutzer in ein neues Format zu zwingen
Wenn man schon aus einer wirklich simplen Entscheidung heraus das Konfigurationsformat ändert, dann sollte man zumindest etwas wie YAML-Konfiguration als zusätzliche Option anbieten, statt plötzlich eine neue if-clause-artige Konfigurationssyntax aufzuzwingen
Jetzt, da WHATWG streams im Browser weit verbreitet sind, ist es ziemlich einfach geworden, etwas WebSocket-Ähnliches auf langlebigen HTTP-Requests selbst zu implementieren
Man sendet einfach einen Byte-Stream und setzt vor jede Nachricht einen Header; in vielen Fällen reicht ein einzelner Längenwert
Das hat auch Vorteile. Anders als bei WebSocket braucht man in der Server-Schicht keinen separaten Sonderpfad, man kann Backpressure nutzen, bekommt Verbesserungen aus HTTP/2 und HTTP/3 gratis mit, und der Framing-Overhead ist geringer
Soweit ich weiß, wird es allerdings noch nicht unterstützt, den Request-Body weiter zu streamen und gleichzeitig eine Response zu empfangen, daher braucht man für vollständiges bidirektionales Streaming zwei Requests
Ich habe das alte plain CGI wiederentdeckt, und für unsere Plattform ist es hervorragend geeignet, damit Nutzer eigene Seiten per Vibe Coding erstellen können [1]
Als Grundfunktionen bieten wir eine Task List und einen Data Viewer, aber Nutzer wollen oft viel feinere Anpassungen, etwa eine Kanban-Ansicht oder ein eigenes Dashboard mit Datenfiltern und Charts
In dieser Box gibt es einen Coding Agent, also können Nutzer statt eines traditionellen Report Builders direkt selbst den gewünschten Code erstellen
Die Go-Stdlib unterstützt das sowohl serverseitig als auch im User Space gut, und wenn der Coding Agent
page-name/main.goerstellt und per CGI kommunizieren lässt, delegiert der Server Requests dorthinDa Datenvolumen und Pageviews alle im Person-Scale-Bereich liegen, brauchen wir Optimierungen wie FastCGI gar nicht wirklich
Im Zeitalter der Agents wird alte Technologie wieder neu
Gos CGI-Serverimplementierung setzt
$HTTP_PROXYnicht und ist in diesem Punkt daher sicher, aber mir gefällt trotzdem nicht, wie CGI generell Umgebungsvariablen verwendetAuf der Reverse-Proxy-Seite waren die Aufgaben meist einfach genug, dass die eingebauten Funktionen von Nginx ausgereicht haben
Trotzdem wäre mir die Idee, dafür FastCGI zu verwenden, wohl nie gekommen
Vor etwa 10 Jahren habe ich FastCGI kurz genutzt, um etwas C++-Code über das Web laufen zu lassen, aber seitdem praktisch nicht mehr
Man packt einfach direkt einen HTTP-Server in die Anwendung und erledigt dort, was nötig ist, ganz ohne Gateway
Die mit Red Hat verwandten PHP/Apache-Setups verwenden FPM (FastCGI Process Manager)
Ich weiß nicht, ob FastCGI in RHEL-Distributionen noch an anderer Stelle verwendet wird
$ rpm -qi php-fpm | grep ^SummarySummary : PHP FastCGI Process ManagerIm Fedora-Paket
httpd-coreist es enthalten. Bei RHEL weiß ich es nicht: https://packages.fedoraproject.org/pkgs/httpd/httpd-core/fed...Es gibt auch das uwsgi protocol
Im Grunde hat auch das eher den Charakter eines RPC für fast alles
FCGI ist auch ein Orchestrierungssystem
Wenn die Last steigt, startet es mehr Server-Tasks, bei sinkender Last fährt es sie wieder herunter, und wenn ein Task abstürzt, startet es eine neue Kopie
Eine Art Kubernetes für ein einzelnes System
Das klingt gut, aber oft lief es bei normaler geringer Last problemlos und bei hoher Last wurde dann durch zusätzliche Worker der Speicher leergezogen
Daher war eine statische Worker-Zahl in der Regel besser
Crash Recovery kann aber nützlich sein, wenn man sie braucht
Es lohnt sich, kurz die Absurdität von HTTP-Headern zu würdigen
Wenn
True-Client-IPfehlt und man nur dannX-Real-IPverwendet, kann ein Angreifer einenTrue-Client-IP-Header schicken und damit Erfolg haben, selbst wenn der ProxyX-Real-IPkorrekt setztEs gibt
X-Forwarded-For,X-Real-IPund je nach CDN ganz unterschiedliche Custom Header, manche davon sind kommagetrennte Listen, oft sogar noch nutzloserweise ergänzt um die IP unseres eigenen LBIch verstehe, warum das so ist, aber hilfreich ist es überhaupt nicht
Außerdem können all diese Header von einem bösartigen User-Agent eingeschleust werden. Es wirkt, als hätte sich nie jemand darauf geeinigt, wie vertrauenswürdige Server in einer Pipeline wichtige Informationen weitergeben sollen
Dieses Chaos passt auch gut zur Absurdität des User-Agent-Headers
Dort ist es noch extremer geworden, seit Apple im Namen der Privatsphäre beschlossen hat, komplett falsche Informationen zu senden, etwa erfundene OS-Versionen
An dieser Behauptung ist viel Wahres dran, aber FastCGI folgt bei Dingen wie
PATH_INFOCGI/1.1, wodurch Informationen verloren gehenURL-Decoding wird erzwungen, sodass ein encoded slash wie
%2Fnicht dargestellt werden kannJe nach Implementierung wird
//im Pfad auch zu/zusammengezogen; dieses Problem gibt es allerdings auch in verschiedenen HTTP-ImplementierungenIn Sachen Ausdruckskraft ist es HTTP unterlegen, und ob dieser Unterschied wichtig ist, hängt von der Anwendung ab
Ich bevorzuge es, URLs präzise zu behandeln