- 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/fcgicủa Go điềnREMOTE_ADDRvàoRequest.RemoteAddrvà phản ánh trạng thái HTTPS vàoRequest.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/fcgivà thayhttp.Servebằngfcgi.Serve- Handler hiện có vẫn dùng nguyên
http.ResponseWritervàhttp.Request - Phần còn lại trong cấu trúc của ứng dụng cũng được giữ nguyên
- Handler hiện có vẫn dùng 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"
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-IPthườ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-IPtrước khi xác định IP thực của client, và chỉ dùngX-Real-IPnế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ửiTrue-Client-IP
- Vì thế, ngay cả khi proxy xử lý
- FastCGI tách biệt header của client và thô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
- 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ố
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/fcgicủa Go tự động điền giá trị này vàoRemoteAddrcủahttp.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
TLScủaRequestthà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
- 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
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/…
Ý 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 pipeKhô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
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ềuViệ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
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í 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
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ẻ
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ườngPhí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
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 ^SummarySummary : PHP FastCGI Process ManagerNó có trong gói
httpd-corecủa Fedora. Còn RHEL thì tôi không rõ: https://packages.fedoraproject.org/pkgs/httpd/httpd-core/fed...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ẻ
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ỉ cần ngắm qua một chút sự phi lý của HTTP header là đủ
Nếu chỉ dùng
X-Real-IPkhi không cóTrue-Client-IP, thì ngay cả khi proxy đã chènX-Real-IPđúng cách, kẻ tấn công vẫn có thể gửiTrue-Client-IPheader để qua mặt bạnCó
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ôiTô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_INFOvì nó tuân theo CGI/1.1Việc URL decoding là bắt buộc khiến không thể biểu diễn encoded slash là
%2FTù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 HTTPXé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