- Como protocolo de proxy para pasar solicitudes por socket a backends de larga ejecución, puede aplicarse casi sin cambiar la estructura existente de los handlers HTTP
- El proxy inverso HTTP/1.1 tiende a producir discrepancias en la interpretación de los límites de los mensajes entre implementaciones, lo que sigue generando problemas graves de seguridad como desync y request smuggling
- FastCGI ha proporcionado un framing de mensajes claro desde 1996, y separa estructuralmente los headers del cliente de la información confiable añadida por el proxy
- En Go,
net/http/fcgillenaRequest.RemoteAddrconREMOTE_ADDRy también refleja si se usa HTTPS enRequest.TLS, por lo que la transmisión de información confiable puede resolverse sin middleware adicional - Aunque tiene limitaciones como la falta de soporte para WebSockets, un ecosistema de herramientas débil y menor rendimiento en algunas cargas de trabajo, sigue pareciendo una opción práctica si no se necesitan WebSockets y el rendimiento es suficiente
La posición de FastCGI y cómo aplicarlo
- FastCGI no se usa solo para el modelo de ejecutar procesos por archivo, sino también como protocolo proxy-backend para enviar solicitudes por TCP o sockets UNIX a demonios de larga ejecución
- En Go, puede aplicarse simplemente importando el paquete
net/http/fcgiy cambiandohttp.Serveporfcgi.Serve- Los handlers existentes siguen usando
http.ResponseWriteryhttp.Requesttal como están - El resto de la estructura de la aplicación también se mantiene igual
- Los handlers existentes siguen usando
- Los principales proxys como Apache, Caddy, nginx y HAProxy soportan backends FastCGI, y la configuración suele ser relativamente simple
Problemas de parsing al usar HTTP como protocolo de backend
- El reverse proxying con HTTP se parece a un campo minado de seguridad, y siguen apareciendo problemas como la vulnerabilidad de desync en el proxy de medios de Discord, que incluso puede permitir espiar archivos adjuntos privados
- HTTP/1.1 parece a simple vista un protocolo de texto sencillo, pero tiene demasiadas formas de expresar el mismo mensaje y demasiados casos especiales, por lo que es fácil que cada implementación lo interprete distinto
- El mayor problema es que los mensajes HTTP no tienen framing explícito
- El final del mensaje se describe de múltiples maneras dentro del propio mensaje
- Cada implementación puede interpretar de forma distinta dónde termina un mensaje y dónde empieza el siguiente
- Estas discrepancias son la base de HTTP desync attacks o request smuggling, y generan problemas graves de seguridad cuando el proxy inverso y el backend entienden de forma distinta los límites del mensaje
- Seguir parchando diferencias entre parsers difícilmente puede ser una solución de fondo
- James Kettle sigue encontrando nuevos tipos de ataque
- Después de hallar casos adicionales el año pasado, incluso usó la expresión "HTTP/1.1 must die"
Cómo manejan FastCGI y HTTP/2 los límites de mensaje
- HTTP/2 puede resolver los problemas de desync al definir claramente los límites de los mensajes si se usa de forma consistente entre el proxy y el backend
- FastCGI lleva ofreciendo esta separación clara de límites desde 1996 con un protocolo más simple
- nginx soporta backends FastCGI desde su primera versión, pero el soporte para backends HTTP/2 recién se añadió a finales de 2025
- El soporte de Apache para backends HTTP/2 sigue en estado "experimental"
El problema de los headers no confiables y la forma en que FastCGI los separa
- No es solo un problema de desync: HTTP también carece de una forma sólida de transportar datos que el proxy debe confiar y reenviar, como la IP real del cliente, el nombre de usuario autenticado por el proxy o la información del certificado del cliente en mTLS
- En la práctica, esta información suele colocarse en headers HTTP, pero no existe una separación estructural entre los datos confiables añadidos por el proxy y los headers no confiables enviados por el cliente
- Headers como
X-Real-IPse usan con frecuencia para pasar la IP real del cliente, pero solo son seguros si el proxy elimina completamente todos los headers preexistentes y luego los vuelve a agregar, incluyendo variaciones de mayúsculas y minúsculas - Este enfoque es un terreno muy peligroso, y hay muchas formas de que el backend termine confiando en datos introducidos por un atacante
- El proxy debe borrar no solo
X-Real-IP, sino cualquier header usado con este propósito - Por ejemplo, el middleware de Chi revisa primero
True-Client-IPal determinar la IP real del cliente, y solo usaX-Real-IPsi aquel no existe- Incluso si el proxy maneja correctamente
X-Real-IP, puede haber problemas si un atacante envíaTrue-Client-IP
- Incluso si el proxy maneja correctamente
- FastCGI separa los headers del cliente y la información añadida por el proxy mediante separación de dominios
- Ambos se envían como listas de parámetros clave/valor, pero los nombres de headers HTTP llevan el prefijo
HTTP_ - Por lo tanto, no se da una estructura en la que un header enviado por el cliente pueda interpretarse como datos confiables del proxy
- Ambos se envían como listas de parámetros clave/valor, pero los nombres de headers HTTP llevan el prefijo
Manejo de información confiable con FastCGI en Go
- FastCGI define parámetros estándar como
REMOTE_ADDRpara transmitir la IP real del cliente net/http/fcgide Go llena automáticamenteRemoteAddrdehttp.Requestcon este valor, por lo que funciona sin middleware adicional- El proxy también puede transmitir como parámetros no estándar información como si se usó HTTPS, la cipher suite TLS negociada o el certificado del cliente
- En Go, si la solicitud usó HTTPS, el campo
TLSdeRequestse establece automáticamente con un valor distinto de nil- Aunque esté vacío, resulta útil para verificar si debe forzarse HTTPS
- Con
fcgi.ProcessEnvse puede acceder al conjunto completo de parámetros confiables enviados por el proxy
Por qué su adopción ha sido lenta y cuáles son sus límites reales
- Si FastCGI es mejor, ¿por qué no se usa más? Parece que influyen tanto el aire anticuado del nombre como la falta de conciencia sobre los problemas de seguridad del reverse proxying con HTTP
- Watchfire ya trató los ataques de desync en 2005 y advirtió que no serían fáciles de resolver, pero estos ataques no recibieron atención real durante más de una década
- FastCGI sigue siendo utilizable en la práctica hoy en día, y en SSLMate lo usan en producción desde hace más de 10 años
- Aun así, al ser una tecnología antigua, también tiene debilidades
- No se ha actualizado para soportar WebSockets
- Su ecosistema de herramientas es limitado
- Por ejemplo, curl soporta hasta FTP, Gopher y SMTP, pero no puede enviar solicitudes FastCGI
- Al hacer benchmarks de un servidor FastCGI en Go detrás de varios reverse proxies, algunas cargas de trabajo mostraron menor throughput que HTTP/1.1 o HTTP/2
- Esto se interpreta no tanto como una limitación inherente del protocolo, sino como resultado de que la ruta de código de FastCGI no está tan optimizada como la de HTTP
Juicio final
- Si no se necesitan WebSockets y el rendimiento actual es suficiente, FastCGI sigue siendo una opción que vale la pena
- Incluso si aparece un cuello de botella, parece preferible agregar más hardware antes que asumir la complejidad y la pesadilla de seguridad del reverse proxying con HTTP
2 comentarios
Me impresionó el comentario sobre FastCGI de Twisted que encontré en los comentarios de Lobsters: https://web.archive.org/web/20160723091923/…
Opiniones en Hacker News
Estoy de acuerdo con la idea del artículo. Para este tipo de uso, FastCGI me parece mejor que HTTP
También quisiera dar a conocer un protocolo llamado WAS (Web Application Socket). Hace 16 años, en mi trabajo sentí que incluso FastCGI no era lo bastante bueno, así que lo diseñé yo mismo
En lugar de usar framing en el socket principal, usa 1 socket de control y 2 pipes para los cuerpos raw de solicitud/respuesta, y tanto la app WAS como el servidor web pueden aprovechar
splice()sobre los pipesNo hace falta framing, permite cancelar solicitudes y se diseñó para que siempre se puedan recuperar los tres file descriptors
Lo he usado durante años en aplicaciones internas y en entornos de web hosting, y también escribí yo mismo un PHP SAPI. Bastantes sitios web funcionan internamente sobre WAS
Todo es 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 sirve para transportar datos entre los dos extremos, como el navegador y el servidor, mientras que FastCGI se usa para procesar esos datos entre el servidor y la aplicación
Acabo de hojear el artículo y da la impresión de que el autor los presenta de forma confusa, como si fueran reemplazables entre sí. En realidad, no lo son en absoluto
Por cierto, yo también he usado fcgi durante 10 años en servicios web para clientes
Este artículo es interesante precisamente porque deja fuera muchas cosas
Cuando estaba en pleno debate FastCGI vs. SCGI vs. HTTP, fundé una startup Web2.0 y armé yo mismo el stack de frontend; al final HTTP ganó por una razón simple: la simplicidad
Si ya ibas a procesar HTTP en el gateway, usarlo tal cual evitaba tener que meter otro protocolo más en el stack, y eso hacía muy fácil encadenar varias capas de reverse proxy o separar intereses transversales como autenticación, sesión, terminación SSL y filtrado DDoS en servidores con roles específicos
En desarrollo, podías conectarte directamente por HTTP al servidor de la app, y en producción reutilizar exactamente ese mismo servidor mientras el reverse proxy se encargaba de SSL, autenticación y detección de abuso
En ese momento también influyó mucho que nginx era bastante más rápido y estable que la mayoría de los módulos FastCGI/SCGI. Al principio teníamos
HTTP -> Lighttpd -> FastCGI -> Django, pero usar nginx directamente era mucho más rápidoUsar HTTP funcionaba como una versión web del End-to-End Principle. La idea es que la red y el protocolo deberían ser indiferentes al contenido transportado, y que la lógica de la aplicación debería estar en los extremos, no en nodos intermedios de la red que filtran o redirigen
Aun así, el punto central que señala el artículo es que, desde la seguridad, muchas veces conviene más seguir el principio de mínimo privilegio. Hay que dejar pasar en una allowlist solo la comunicación esperada para no contribuir sin querer a una intrusión en otro punto
Al final hay una tensión entre ambas cosas. E2E da flexibilidad, pero esa flexibilidad también amplía el espacio para abusos; PoLP da seguridad, pero te deja hacer solo lo que diseñaste y dificulta adaptarse a nuevos requisitos
[1] https://en.wikipedia.org/wiki/End-to-end_principle
[2] https://en.wikipedia.org/wiki/Principle_of_least_privilege
Si un gateway intermedio multiplexa varias solicitudes HTTP sobre otro único canal HTTP, y ese canal llega directo hasta el listening service sin demultiplexarse antes del socket de la aplicación, eso rompe de raíz la lógica end-to-end de varias formas
Esa analogía solo medio se sostiene cuando se conserva la simetría de conexión 1:1
Diría que todas las vulnerabilidades de reverse proxy provienen directamente de romper el principio end-to-end
Si la analogía fuera correcta, entonces la entrega de SMTP pasando por varios MX también tendría que ser end-to-end, pero no lo es, y de hecho aparecen problemas muy parecidos a los de los reverse proxy, como desync en los límites del mensaje
Entiendo la intención de mapear solicitudes HTTP a mensajes, pero eso se rompe rápido por la semántica real de TCP/HTTP y por todo tipo de detalles del protocolo
El principio end-to-end no permite tratar la semántica a la ligera. Exige una disciplina muy estricta con el manejo de estado y con los límites de la capa de transporte. Algo más o menos parecido a end-to-end no es end-to-end
Por ejemplo, ni siquiera tenía multiplexing antes de HTTP 2.0, así que usar HTTP tal cual entre reverse proxy y backend es muy ineficiente
También hay problemas de seguridad. Distintos parsers pueden interpretar de forma distinta incluso dónde termina el límite de una solicitud
Desde hace mucho tiempo, Google también encapsula HTTP entre su servidor web frontal y la aplicación usando su propio protocolo Stubby
Es mucho más rápido y tiene más funciones que el wire protocol de HTTP. Para una empresa normal suele ser demasiado, pero a gran escala sí se justifica el costo de crear tu propio wire protocol y el tooling alrededor
En algún momento httpd también fue en la dirección de volver más difícil la configuración, y lo dejé justo cuando cambió de golpe el formato de configuración
Probablemente podría haberme adaptado, pero en vez de eso migré a lighttpd, y después ruby automatizó la generación de configuración, así que técnicamente hasta podría volver a httpd
Aun así, no quiero regresar. Quienes desarrollan servidores web deberían pensarlo bien antes de forzar a los usuarios a adaptarse a un formato nuevo
Si de verdad van a cambiar el formato de configuración por una decisión supuestamente simple, al menos podrían ofrecer algo como configuración en yaml como opción adicional, en vez de imponer de pronto un nuevo estilo de sentencias tipo if-clause
Ahora que WHATWG streams ya están ampliamente presentes en los navegadores, es bastante fácil implementar una especie de WebSocket propio encima de una solicitud HTTP de larga duración
Solo mandas un stream de bytes y le pones un encabezado a cada mensaje; en muchos casos basta con un solo valor de longitud
También tiene ventajas. No requiere una ruta especial aparte en la capa del servidor como WebSocket, puedes usar backpressure, obtienes gratis las mejoras de HTTP/2 y HTTP/3, y el overhead de framing es menor
Eso sí, AFAIK todavía no se soporta seguir haciendo streaming del body de la solicitud mientras al mismo tiempo recibes la respuesta, así que para streaming bidireccional completo hacen falta dos solicitudes
Redescubrí el viejo plain CGI, y es excelente para permitir que los usuarios hagan vibe code de páginas personalizadas en nuestra plataforma [1]
Tenemos funciones integradas como task list y data viewer, pero a menudo los usuarios quieren personalizaciones mucho más finas, como vistas Kanban o dashboards personalizados con filtros de datos y gráficos
Esta caja tiene un coding agent, así que en vez de que nosotros construyamos un report builder tradicional, el usuario puede programar directamente lo que quiere
La stdlib de Go tiene buen soporte tanto del lado del servidor como del user space, y si el coding agent crea
page-name/main.goy hace que se comunique por CGI, el servidor delega allí la solicitudComo el volumen de datos y los pageviews están todos a escala de persona, ni siquiera hacen falta optimizaciones como FastCGI
En la era de los agentes, la tecnología vieja vuelve a sentirse nueva
La implementación del servidor CGI de Go no configura
$HTTP_PROXY, así que por ese lado es segura, pero aun así no me gusta cómo CGI usa variables de entornoEn el lado del reverse proxy, por lo general solo hacíamos cosas simples, así que con las funciones integradas de Nginx bastaba
Aun así, si hubiera hecho falta algo más complejo, no creo que se me hubiera ocurrido usar FastCGI para eso
Hace unos 10 años sí usé un poco FastCGI para ejecutar parte de código C++ vía web, pero después de eso casi no lo volví a usar
Metes un servidor HTTP directamente en la aplicación y simplemente haces lo que necesites, sin gateway
La configuración de PHP/Apache distribuida en entornos de Red Hat usa FPM (FastCGI Process Manager)
No sé si en distribuciones RHEL se use FastCGI para algo más
$ rpm -qi php-fpm | grep ^SummarySummary : PHP FastCGI Process ManagerViene incluido en el paquete
httpd-corede Fedora. En RHEL no estoy seguro: https://packages.fedoraproject.org/pkgs/httpd/httpd-core/fed...También existe el uwsgi protocol
Esto también es, en esencia, algo parecido a un RPC para casi todo
FCGI también es un sistema de orquestación
Cuando sube la carga, levanta más tareas del servidor; cuando baja, las reduce; y si una tarea se cae, lanza una copia nueva
Es como una especie de Kubernetes de una sola máquina
Suena bien, pero era muy común que funcionara perfecto con poca carga y que, cuando llegaba carga alta, al crear más workers se quedara sin memoria
Por eso normalmente era mejor usar una cantidad estática de workers
Aun así, la recuperación ante caídas sí puede ser útil si la necesitas
Basta con detenerse un momento a apreciar el absurdo de los encabezados HTTP
Si solo usas
X-Real-IPcuando no existeTrue-Client-IP, entonces aunque el proxy agregue correctamenteX-Real-IP, un atacante puede simplemente enviar un encabezadoTrue-Client-IPy ya te afectóEstán
X-Forwarded-For,X-Real-IPy hasta encabezados personalizados distintos según cada CDN; algunos además son listas separadas por comas y normalmente hasta incluyen sin necesidad la IP de nuestro propio LBEntiendo por qué pasa esto, pero no ayuda en nada
Encima, todos esos encabezados pueden ser inyectados por un user-agent malicioso. Parece que nadie logró ponerse de acuerdo sobre cómo deberían transmitir información importante los servidores confiables a través del pipeline
Este caos combina perfectamente con el absurdo del encabezado User-Agent
Ahí Apple lo llevó todavía más lejos al decidir enviar información totalmente falsa, como versiones inventadas del SO, supuestamente por privacidad
Hay bastante de cierto en esta postura, pero FastCGI pierde capacidad expresiva en cosas como
PATH_INFOporque sigue CGI/1.1Obliga a hacer URL decoding y no permite representar un encoded slash como
%2FSegún la implementación, también puede colapsar
//a/en la ruta, aunque ese también es un problema presente en varias implementaciones HTTPEn términos de expresividad, queda por debajo de HTTP, y qué tanto importe esa diferencia depende de la aplicación
Yo prefiero poder manejar las URL con precisión