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

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

  • FastCGI không chỉ dùng cho kiểu thực thi process theo từng file, mà còn có thể dùng như giao thức proxy-backend gửi yêu cầu tới daemon chạy dài hạn qua TCP hoặc UNIX socket
  • Trong Go, có thể áp dụng chỉ bằng cách import gói net/http/fcgi và đổi http.Serve thành fcgi.Serve
    • Handler hiện có vẫn dùng nguyên http.ResponseWriterhttp.Request
    • Phần còn lại của cấu trúc ứ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 bằng 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 có thể tiếp tục cho phép nhìn trộm tệp đính kèm riêng tư
  • HTTP/1.1 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à cũng có nhiều ngoại lệ, nên rất dễ dẫn tới cách diễn giải khác nhau giữa các triển khai
  • Vấn đề lớn nhất là thông điệp HTTP không có framing tường minh
    • Chính thông điệp tự mô tả điểm kết thúc của nó theo nhiều cách khác nhau
    • Các triển khai có thể diễn giải khác nhau về chỗ một thông điệp kết thúc và thông điệp tiếp theo bắt đầu
  • Những khác biệt như vậy là nền tảng cho HTTP desync attacks hoặc request smuggling, nơi reverse proxy và backend hiểu khác nhau về ranh giới thông điệp, từ đó tạo ra vấn đề bảo mật nghiêm trọng
  • Việc liên tục vá khác biệt giữa các parser khó có thể là giải pháp gốc rễ
    • 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 năm ngoái, ông thậm chí đã 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 nếu được dùng nhất quán giữa proxy và backend có thể giải quyết vấn đề desync nhờ xác định ranh giới thông điệp rõ ràng
  • FastCGI đã cung cấp sự phân định ranh giới rõ ràng này bằng một giao thức đơn giản hơn ngay 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 nửa 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ỉ là vấn đề desync, HTTP còn thiếu một cách chắc chắn để mang dữ liệu mà proxy cần tin cậy khi 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 đưa vào HTTP header, nhưng không có sự phân tách có 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 sẵn có rồi thêm lại, kể cả các biến thể khác nhau về chữ hoa chữ thường
  • Đây là một địa hình cực kỳ nguy hiểm, và có rất nhiều con đường khiến backend vô tình tin vào 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à còn phải xóa mọi header dùng cho mục đích kiểu này
  • Ví dụ, middleware Chi khi xác định IP thực của client sẽ kiểm tra True-Client-IP trước, và chỉ dùng X-Real-IP nếu không có header đó
    • Vì vậy, 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 phân tách header của client và thông tin do proxy thêm vào bằng cơ chế tách biệt domain
    • Cả hai đều được truyền dưới dạng danh sách tham số khóa/giá trị, nhưng tên header HTTP sẽ có tiền tố HTTP_
    • Vì vậy, không thể hình thành cấu trúc mà trong đó header do client gửi lạ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 trong FastCGI ở 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 được mà không cần middleware riêng
  • Proxy cũng có thể truyền các thông tin như việc có dùng HTTPS, TLS cipher suite đã thương lượng, hay chứng chỉ client dưới dạng tham số không chuẩn
  • Go tự động đặt trường TLS của Request thành giá trị khác nil khi yêu cầu dùng HTTPS
    • Dù rỗng, điều này vẫn hữu ích để kiểm tra việc bắt buộc dùng HTTPS
  • Có thể truy cập toàn bộ tập tham số tin cậy mà proxy gửi sang bằng fcgi.ProcessEnv

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

  • Nếu FastCGI tốt hơn, tại sao nó không được dùng rộng rãi? Có vẻ nguyên nhân là do cái tên mang cảm giác cũ kỹ cùng với việc nhận thức về vấn đề bảo mật của HTTP reverse proxy còn thấp
  • Watchfire đã nói về tấn công desync từ năm 2005 và cũng cảnh báo rằng vấn đề này không dễ giải quyết, nhưng các kiểu tấn công đó đã không thực sự được chú ý đúng mức trong hơn 10 năm
  • FastCGI đến nay vẫn có thể dùng thực tế, và SSLMate đã dùng nó trong production hơn 10 năm
  • Tuy vậy, vì là công nghệ cũ, nó cũng có điểm yếu
    • Chưa đượ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 yêu cầu FastCGI
  • Khi benchmark server Go FastCGI phía sau nhiều reverse proxy, một số workload có 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 code FastCGI chưa được tối ưu tốt như HTTP, hơn là giới hạn nội tại của giao thức

Kết luận 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 có nút thắt cổ chai, tác giả vẫn cho rằng nên bổ sung phần cứng còn hơn chấp nhận độ 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 2 giờ trước

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 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