2 điểm bởi GN⁺ 2026-04-30 | 2 bình luận | Chia sẻ qua WhatsApp
  • Là một giao thức proxy chuyển tiếp request qua socket tới backend chạy lâu dài, có thể áp dụng mà hầu như không cần thay đổi cấu trúc trình xử lý HTTP hiện có
  • Reverse proxy HTTP/1.1 dễ gặp sai lệch trong cách diễn giải ranh giới thông điệp giữa các implementation, tiếp tục tạo ra các vấn đề bảo mật nghiêm trọng như desync và request smuggling
  • FastCGI đã cung cấp cơ chế framing thông điệp rõ ràng từ năm 1996, đồng thời tách biệt về mặt cấu trúc giữa header của client và thông tin tin cậy do proxy thêm vào
  • net/http/fcgi của Go điền REMOTE_ADDR vào Request.RemoteAddr và phản ánh trạng thái HTTPS vào Request.TLS, nhờ đó xử lý truyền thông tin tin cậy mà không cần middleware riêng
  • Dù có những hạn chế như không hỗ trợ WebSockets, hệ sinh thái công cụ yếu và throughput thấp hơn ở một số workload, đây vẫn là một lựa chọn thực tế nếu không cần WebSockets và hiệu năng hiện tại là đủ

Vị trí và cách áp dụng FastCGI

  • FastCGI không chỉ dùng cho kiểu thực thi tiến trình theo từng file, mà còn có thể dùng làm giao thức proxy-backend gửi request qua TCP hoặc UNIX socket tới daemon chạy lâu dài
  • Trong Go, có thể áp dụng chỉ bằng cách import gói net/http/fcgi và thay http.Serve bằng fcgi.Serve
    • Handler hiện có vẫn dùng nguyên http.ResponseWriterhttp.Request
    • Phần còn lại trong cấu trúc của ứng dụng cũng được giữ nguyên
  • Các proxy phổ biến như Apache, Caddy, nginx, HAProxy đều hỗ trợ backend FastCGI và cấu hình cũng khá đơn giản

Vấn đề parsing khi dùng HTTP làm giao thức backend

  • Reverse proxy HTTP gần như là một bãi mìn bảo mật, và các vấn đề như lỗ hổng desync trong media proxy của Discord, vốn có thể làm lộ tệp đính kèm riêng tư, vẫn tiếp tục xuất hiện
  • HTTP/1.1 nhìn bề ngoài là một giao thức văn bản đơn giản, nhưng có quá nhiều cách biểu diễn cùng một thông điệp và quá nhiều trường hợp ngoại lệ, khiến mỗi implementation dễ diễn giải khác nhau
  • Vấn đề lớn nhất là thông điệp HTTP không có framing tường minh
    • Phần cuối của thông điệp được chính thông điệp mô tả theo nhiều cách khác nhau
    • Mỗi implementation có thể hiểu khác nhau về điểm kết thúc của thông điệp hiện tại và điểm bắt đầu của thông điệp tiếp theo
  • Những khác biệt này trở thành nền tảng cho HTTP desync attacks hoặc request smuggling, tạo ra các vấn đề bảo mật nghiêm trọng khi reverse proxy và backend hiểu ranh giới thông điệp khác nhau
  • Việc tiếp tục vá các khác biệt giữa parser khó có thể trở thành giải pháp tận gốc
    • James Kettle vẫn liên tục tìm ra các biến thể mới
    • Sau khi phát hiện thêm trường hợp trong năm ngoái, ông còn dùng cụm từ "HTTP/1.1 must die"
    Quảng cáo

Cách FastCGI và HTTP/2 xử lý ranh giới thông điệp

  • HTTP/2 có thể giải quyết vấn đề desync bằng cách làm rõ ranh giới thông điệp nếu được dùng nhất quán giữa proxy và backend
  • FastCGI đã cung cấp cách phân định ranh giới rõ ràng như vậy bằng một giao thức đơn giản hơn, từ năm 1996
  • nginx hỗ trợ backend FastCGI từ bản phát hành đầu tiên, nhưng hỗ trợ backend HTTP/2 chỉ được bổ sung vào cuối năm 2025
  • Hỗ trợ backend HTTP/2 của Apache vẫn còn ở trạng thái "experimental"

Vấn đề header không đáng tin và cách FastCGI tách biệt

  • Không chỉ có desync, HTTP còn thiếu một cách vững chắc để mang các dữ liệu mà proxy cần tin cậy và chuyển tiếp, như IP thực của client, tên người dùng đã được proxy xác thực, hay thông tin chứng chỉ client trong mTLS
  • Trên thực tế, các thông tin này thường được đặt trong HTTP header, nhưng không có sự phân tách về mặt cấu trúc giữa dữ liệu tin cậy do proxy thêm vào và header không đáng tin do client gửi lên
  • Các header như X-Real-IP thường được dùng để chuyển IP thực của client, nhưng để an toàn, proxy phải xóa hoàn toàn mọi header có sẵn cùng mục đích, kể cả các biến thể khác nhau về chữ hoa/chữ thường, rồi mới thêm lại
  • Đây là một địa hình cực kỳ nguy hiểm, với rất nhiều con đường khiến backend tin nhầm dữ liệu do kẻ tấn công chèn vào
  • Proxy không chỉ phải xóa X-Real-IP, mà phải xóa mọi header dùng cho mục đích kiểu này
  • Ví dụ, middleware Chi sẽ kiểm tra True-Client-IP trước khi xác định IP thực của client, và chỉ dùng X-Real-IP nếu header kia không tồn tại
    • Vì thế, ngay cả khi proxy xử lý X-Real-IP đúng cách, kẻ tấn công vẫn có thể gây vấn đề bằng cách gửi True-Client-IP
  • FastCGI tách biệt header của clientthông tin do proxy thêm vào theo cách phân tách miền
    • Cả hai đều được truyền dưới dạng danh sách tham số khóa/giá trị, nhưng tên HTTP header luôn có tiền tố HTTP_
    • Vì vậy, không thể hình thành tình huống mà header do client gửi bị diễn giải thành dữ liệu tin cậy của proxy

Xử lý thông tin tin cậy với FastCGI trong Go

  • FastCGI định nghĩa các tham số chuẩn như REMOTE_ADDR để truyền IP thực của client
  • net/http/fcgi của Go tự động điền giá trị này vào RemoteAddr của http.Request, nên hoạt động mà không cần middleware riêng
  • Proxy cũng có thể chuyển các thông tin như trạng thái sử dụng HTTPS, TLS cipher suite đã thương lượng, hay chứng chỉ client dưới dạng tham số phi chuẩn
  • Go tự động đặt trường TLS của Request thành giá trị khác nil khi request dùng HTTPS
    • Điều này vẫn hữu ích để kiểm tra việc bắt buộc dùng HTTPS, ngay cả khi nội dung bên trong trống
    Quảng cáo
  • Có thể truy cập toàn bộ tập tham số tin cậy do proxy gửi bằng fcgi.ProcessEnv

Vì sao mức độ phổ biến vẫn chậm và những giới hạn thực tế

  • Nếu FastCGI tốt hơn, vì sao nó chưa được dùng rộng rãi? Có vẻ nguyên nhân đến từ cảm giác lỗi thời ngay trong tên gọi và việc thiếu nhận thức về các vấn đề bảo mật của reverse proxy HTTP
  • Watchfire đã đề cập đến desync attack từ năm 2005 và cũng cảnh báo rằng đây không phải vấn đề dễ giải quyết, nhưng kiểu tấn công này đã không thực sự được chú ý trong hơn 10 năm
  • FastCGI đến nay vẫn dùng được trong thực tế, và SSLMate đã vận hành nó trong production hơn 10 năm
  • Tuy vậy, đây vẫn là công nghệ cũ nên cũng có nhược điểm
    • Không được cập nhật để hỗ trợ WebSockets
    • Hệ sinh thái công cụ còn thiếu
    • Ví dụ, curl hỗ trợ cả FTP, Gopher, SMTP nhưng lại không thể gửi request FastCGI
  • Khi benchmark server FastCGI của Go phía sau nhiều reverse proxy khác nhau, một số workload cho thấy throughput thấp hơn HTTP/1.1 hoặc HTTP/2
    • Điều này được xem là hệ quả của việc đường mã FastCGI chưa được tối ưu mạnh như HTTP, hơn là giới hạn cố hữu của giao thức

Đánh giá cuối cùng

  • Nếu không cần WebSockets và hiệu năng hiện tại là đủ, FastCGI vẫn là một lựa chọn đáng dùng
  • Ngay cả khi sau này xuất hiện bottleneck, tác giả vẫn cho rằng thêm phần cứng còn tốt hơn là chấp nhận sự phức tạp và cơn ác mộng bảo mật của HTTP reverse proxying

2 bình luận

 
rtyu1120 2026-04-30

Bình luận về FastCGI của Twisted mà tôi tìm thấy trong phần bình luận trên Lobsters khá ấn tượng: https://web.archive.org/web/20160723091923/…

 
GN⁺ 2026-04-30
Ý kiến trên Hacker News
  • Tôi đồng ý với chủ ý của bài viết. Với mục đích này, tôi cho rằng FastCGI tốt hơn HTTP
    Tôi cũng muốn giới thiệu giao thức WAS (Web Application Socket). 16 năm trước ở chỗ làm, tôi cảm thấy ngay cả FastCGI cũng chưa đủ tốt nên đã tự thiết kế nó
    Thay vì framing trên socket chính, nó dùng 1 socket điều khiển và 2 pipe cho phần thân request/response thô, và cả ứng dụng WAS lẫn web server đều có thể tận dụng splice() với các pipe
    Không cần framing, có thể hủy request, và tôi cũng thiết kế để luôn có thể khôi phục ba file descriptor đó
    Tôi đã dùng nó nhiều năm trong các ứng dụng nội bộ và môi trường web hosting, đồng thời cũng tự viết cả PHP SAPI. Khá nhiều website đang nội bộ chạy trên WAS
    Tất cả đều là mã nguồn mở
    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

    • FastCGI và HTTP không ở cùng một tầng
      HTTP dùng để truyền dữ liệu giữa hai đầu như trình duyệt và máy chủ, còn FastCGI dùng để xử lý dữ liệu đó giữa máy chủ và ứng dụng
      Tôi vừa lướt qua bài viết và có cảm giác tác giả đang viết theo cách dễ làm người đọc nhầm rằng hai thứ này có thể thay thế cho nhau. Thực tế thì hoàn toàn không phải vậy
      Nhân tiện, tôi cũng đã dùng fcgi suốt 10 năm cho dịch vụ web hướng tới khách hàng
  • Bài này thú vị chính vì nó bỏ sót khá nhiều thứ
    Khi cuộc tranh luận FastCGI vs. SCGI vs. HTTP còn đang rất sôi nổi, tôi đã lập một startup Web2.0 và tự dựng frontend stack, và cuối cùng HTTP thắng vì sự đơn giản
    Nếu dùng luôn HTTP mà gateway vốn dĩ đã phải xử lý, thì không cần thêm một giao thức khác vào stack, nhờ đó việc chèn nhiều lớp reverse proxy hoặc tách các mối quan tâm cắt ngang như xác thực, session, SSL termination, lọc DDoS sang các máy chủ chuyên trách trở nên rất dễ
    Trong môi trường phát triển, có thể kết nối thẳng tới app server bằng HTTP; còn khi vận hành thì reverse proxy đảm nhiệm SSL, xác thực và phát hiện lạm dụng, nhờ vậy vẫn tái sử dụng nguyên app server đó
    Khi ấy nginx cũng nhanh và ổn định hơn hẳn phần lớn module FastCGI/SCGI. Ban đầu chúng tôi dùng HTTP -> Lighttpd -> FastCGI -> Django, nhưng cứ dùng nginx thì nhanh hơn nhiều
    Việc dùng HTTP hoạt động giống như Nguyên lý đầu-cuối phiên bản web. Tức là mạng và giao thức nên độc lập với nội dung được truyền, còn logic ứng dụng nên nằm ở điểm cuối chứ không phải ở các nút mạng chỉ làm lọc hay chuyển hướng
    Tuy vậy, điểm cốt lõi mà bài viết nêu ra là về mặt bảo mật, nhiều khi làm theo nguyên lý đặc quyền tối thiểu sẽ tốt hơn. Chỉ nên cho phép đúng những luồng giao tiếp đã dự kiến bằng allowlist để tránh vô tình tiếp tay cho việc xâm nhập ở nơi khác
    Rốt cuộc luôn có sự căng thẳng giữa hai hướng này. E2E đem lại tính linh hoạt nhưng cũng mở rộng khả năng bị lạm dụng, còn PoLP đem lại bảo mật nhưng khiến hệ thống chỉ làm được những gì đã thiết kế, nên khó thích nghi với yêu cầu mới
    [1] https://en.wikipedia.org/wiki/End-to-end_principle
    [2] https://en.wikipedia.org/wiki/Principle_of_least_privilege

    • Tôi không nghĩ phép so sánh đó phù hợp, nhất là trong bối cảnh connection caching và multiplexing
      Nếu một gateway trung gian multiplex nhiều request HTTP vào một kênh HTTP khác, và kênh đó đi thẳng tới listening service mà không được demultiplex trước socket ứng dụng, thì điều đó về bản chất đã phá vỡ logic end-to-end theo nhiều cách
      Phép so sánh đó chỉ còn tạm đứng vững nếu vẫn giữ được tính đối xứng kết nối 1:1
      Theo tôi, các lỗ hổng reverse proxy đều bắt nguồn trực tiếp từ việc phá vỡ end-to-end
      Nếu phép so sánh đó đúng, thì việc chuyển phát SMTP qua nhiều MX cũng phải là end-to-end, nhưng thực tế không phải vậy, và nó cũng phát sinh nhiều vấn đề tương tự reverse proxy, chẳng hạn như desync ở ranh giới thông điệp
      Tôi hiểu ý định muốn ánh xạ request HTTP thành thông điệp, nhưng nó nhanh chóng sụp đổ vì ngữ nghĩa thực tế của TCP, HTTP và đủ loại chi tiết giao thức
      Nguyên lý end-to-end không cho phép xử lý hời hợt ngữ nghĩa. Nó đòi hỏi kỷ luật rất nghiêm ngặt trong quản lý trạng thái và ranh giới của tầng truyền tải. Thứ gì đó na ná end-to-end thì không phải end-to-end
    • Với lập trình viên webapp, HTTP semantics rất hữu ích, nhưng bản thân HTTP wire protocol thì tệ
      Ví dụ, multiplexing còn không có trước HTTP 2.0, nên dùng nguyên HTTP cho giao tiếp giữa reverse proxy và backend là khá lãng phí
      Cũng có vấn đề bảo mật. Các parser có thể diễn giải khác nhau về chỗ ranh giới request kết thúc
      Google từ lâu cũng đã bọc HTTP giữa web server phía trước và ứng dụng bằng giao thức riêng Stubby
      Nó nhanh hơn nhiều và có nhiều tính năng hơn HTTP wire protocol. Với đa số công ty thì hơi quá tay, nhưng khi hệ thống đủ lớn, chi phí tự làm một wire protocol khác cùng bộ công cụ xung quanh nó hoàn toàn có thể được biện minh
    • Việc áp dụng end-to-end principle bên trong datacenter gần như không có nhiều ý nghĩa, và như bài viết cho thấy, thậm chí còn cho phép những hành vi kém an toàn hơn
    • Điều tôi không thích ở nginx là tài liệu. Tôi thấy nó gần như chẳng giúp ích mấy
      httpd rồi cũng đi theo hướng khiến cấu hình trở nên khó hơn, và tôi đã bỏ nó vào lúc họ đột ngột thay đổi format cấu hình
      Tôi có thể thích nghi, nhưng thay vào đó tôi chuyển sang lighttpd, rồi sau này ruby tự động hóa việc sinh cấu hình nên về mặt kỹ thuật tôi có thể quay lại httpd
      Dù vậy tôi vẫn không muốn quay lại. Nếu là nhà phát triển web server thì nên thận trọng với chuyện ép người dùng phải theo format mới
      Nếu định thay đổi format cấu hình chỉ vì một quyết định tưởng như đơn giản, thì ít nhất hãy cung cấp thêm tùy chọn như cấu hình yaml, thay vì đột ngột ép mọi người dùng kiểu if-clause mới
  • Giờ đây khi WHATWG streams đã phổ biến rộng trong trình duyệt, việc tự triển khai một thứ giống WebSocket trên các request HTTP sống lâu đã khá dễ
    Chỉ cần gửi một byte stream và gắn header trước mỗi thông điệp; trong nhiều trường hợp chỉ cần một giá trị độ dài là đủ
    Nó cũng có ưu điểm. Không cần một đường xử lý đặc biệt riêng ở tầng server như WebSocket, có thể dùng backpressure, được hưởng miễn phí các cải tiến của HTTP/2 và HTTP/3, và overhead framing cũng thấp hơn
    Tuy nhiên, theo AFAIK thì hiện vẫn chưa hỗ trợ vừa tiếp tục stream request body vừa đồng thời nhận response, nên muốn streaming hai chiều hoàn chỉnh thì cần hai request

  • Tôi mới phát hiện lại plain CGI cũ, và nó rất phù hợp để cho người dùng vibe code các trang tùy chỉnh trên nền tảng của chúng tôi [1]
    Tính năng dựng sẵn thì có task list và data viewer, nhưng người dùng thường muốn những tùy biến chi tiết hơn nhiều, như chế độ xem Kanban hoặc dashboard tùy chỉnh có bộ lọc dữ liệu và biểu đồ
    Trong hộp này có coding agent, nên thay vì chúng tôi làm một report builder kiểu truyền thống, người dùng có thể tự viết đúng thứ họ muốn bằng code
    Go stdlib hỗ trợ tốt ở cả phía server lẫn user space, và khi coding agent tạo page-name/main.go để giao tiếp qua CGI thì server sẽ chuyển request tới đó
    Vì quy mô dữ liệu và pageview đều chỉ ở mức person scale, nên cũng không thật sự cần tối ưu kiểu FastCGI
    Trong kỷ nguyên agent, công nghệ cũ lại trở nên mới mẻ

    1. https://housecat.com
    • Cần lưu ý rằng khác với FastCGI, CGI truyền HTTP header qua biến môi trường, và đây là một cái bẫy khá lớn: https://httpoxy.org/
      Phần triển khai CGI server của Go không thiết lập $HTTP_PROXY, nên ở điểm đó thì an toàn, nhưng tôi vẫn không thích cách CGI dùng biến môi trường
  • Phía reverse proxy thường chỉ làm những việc khá đơn giản nên chỉ dùng tính năng tích hợp sẵn của Nginx là đủ
    Dù vậy, nếu cần thứ gì phức tạp hơn thì ý tưởng dùng FastCGI chắc sẽ không xuất hiện trong đầu tôi
    Khoảng 10 năm trước tôi có thử dùng FastCGI một chút để chạy một phần mã C++ trên web, nhưng từ đó về sau thì gần như không dùng nữa

    • Dạo này embedded server phổ biến hơn nhiều
      Chỉ cần nhúng thẳng HTTP server vào ứng dụng và xử lý luôn những gì cần làm, không cần gateway
  • Cấu hình PHP/Apache được phân phối trong hệ Red Hat là FPM (FastCGI Process Manager)
    Tôi không rõ trên các bản phân phối RHEL người ta còn dùng FastCGI ở chỗ nào khác không
    $ rpm -qi php-fpm | grep ^Summary
    Summary : PHP FastCGI Process Manager

  • Cũng có uwsgi protocol
    Cái này về cơ bản cũng gần như là RPC cho hầu hết mọi thứ

  • FCGI cũng là một hệ thống điều phối
    Khi tải tăng, nó khởi chạy thêm server task; khi tải giảm thì hạ bớt; task chết thì nó khởi chạy bản sao mới
    Kiểu như Kubernetes cho một máy đơn lẻ

    • Theo kinh nghiệm của tôi thì tính năng đó không tốt lắm
      Nghe thì hay, nhưng thường xảy ra chuyện lúc tải thấp thì chạy ổn, đến khi tải cao thì nó sinh thêm worker rồi làm cạn sạch bộ nhớ
      Vì vậy, dùng số lượng worker tĩnh thường lại tốt hơn
      Tuy nhiên, crash recovery vẫn hữu ích nếu bạn cần
    • Chúng tôi cũng đã dùng đúng theo cách đó
  • Chỉ cần ngắm qua một chút sự phi lý của HTTP header là đủ
    Nếu chỉ dùng X-Real-IP khi không có True-Client-IP, thì ngay cả khi proxy đã chèn X-Real-IP đúng cách, kẻ tấn công vẫn có thể gửi True-Client-IP header để qua mặt bạn
    X-Forwarded-For, X-Real-IP, rồi cả các header tùy biến khác nhau theo từng CDN; có cái là danh sách phân tách bằng dấu phẩy, và thường còn vô ích kèm cả IP của LB nội bộ của chúng tôi
    Tôi hiểu vì sao lại thành ra như vậy, nhưng điều đó chẳng giúp ích gì
    Hơn nữa, toàn bộ các header này đều có thể bị user-agent độc hại chèn vào. Cứ như thể chẳng ai thống nhất được cách các server đáng tin cậy nên truyền thông tin quan trọng qua pipeline ra sao
    Sự hỗn loạn này cũng rất hợp với sự phi lý của header User-Agent
    Bên đó còn đi xa hơn nữa khi Apple, lấy lý do quyền riêng tư, quyết định gửi thông tin hoàn toàn giả mạo, chẳng hạn như phiên bản OS bịa đặt

  • Lập luận này có nhiều điểm đúng, nhưng FastCGI bị mất mát ở những phần như PATH_INFO vì nó tuân theo CGI/1.1
    Việc URL decoding là bắt buộc khiến không thể biểu diễn encoded slash%2F
    Tùy theo cách triển khai, // trong đường dẫn cũng có thể bị gộp thành /, dù đây cũng là vấn đề có trong nhiều triển khai HTTP
    Xét về sức biểu đạt thì nó kém hơn HTTP, còn việc khác biệt đó có quan trọng hay không thì tùy ứng dụng
    Cá nhân tôi thích cách xử lý URL chính xác hơn