15 điểm bởi outsideris 2021-03-21 | 1 bình luận | Chia sẻ qua WhatsApp

Vào ngày 8 tháng 3, GitHub đã đăng xuất toàn bộ người dùng trên GitHub.com do một lỗ hổng bảo mật.

  • Ngày 2 tháng 3, có báo cáo rằng một người dùng đã đăng nhập nhưng lại được xác thực thành người dùng khác. Người dùng này đã lập tức đăng xuất, đồng thời báo cáo sự việc và cuộc điều tra được bắt đầu ngay. Vài giờ sau, một người dùng khác báo cáo vấn đề tương tự.

  • Kết quả điều tra ban đầu phát hiện session của một người dùng đã bị chia sẻ từ 2 địa chỉ IP tại thời điểm được báo cáo.

  • Khi điều tra các thay đổi hạ tầng gần đây, họ phát hiện đã nâng cấp phần load balancer và routing, đồng thời HTTP keepalive cũng đã được chỉnh sửa, nên ban đầu có vẻ liên quan, nhưng điều tra thêm thì xác nhận là không liên quan.

  • Tuy vậy, trong quá trình điều tra hạ tầng, họ nhận ra rằng các request nhận nhầm session đều được xử lý trên chính cùng một máy và cùng một process.

  • Sau khi xem log, họ phát hiện response body là bình thường và chỉ có cookie bị gửi sai; cookie của một người dùng khác được xử lý trong cùng process đã bị trả xuống nhầm. Trong một trường hợp được báo cáo, hai request là liên tiếp; ở trường hợp còn lại thì có thêm 2 request khác xen giữa.

  • Từ đây, họ hình thành giả thuyết rằng trạng thái đã bị rò rỉ giữa các request được xử lý trong cùng một process Ruby, và bắt đầu tìm hiểu điều đó có thể xảy ra bằng cách nào.

  • Sau khi rà soát các thay đổi gần đây, họ phát hiện logic kiểm tra các tính năng được kích hoạt cho người dùng, vốn trước đó chạy trong lúc xử lý request, đã được chuyển sang một background thread cập nhật theo chu kỳ để cải thiện hiệu năng. Cuộc điều tra tập trung vào hành vi thread-safe này.

  • Ứng dụng chính của GitHub.com là Ruby on Rails, và có nhiều component không được viết để chạy trong môi trường đa luồng.

  • Thread vốn đã được sử dụng trong ứng dụng, nhưng background thread mới này đã tạo ra hành vi không ngờ tới trong routine xử lý ngoại lệ.

  • Khi ngoại lệ xảy ra trong background thread, log lỗi chứa cả thông tin của background thread lẫn thông tin của request đang chạy.

  • Ban đầu, họ cho rằng việc dữ liệu của request không liên quan xuất hiện trong log từ background thread chỉ là một sự không nhất quán trong hệ thống báo cáo nội bộ.

  • Họ nghĩ rằng Rails an toàn vì tạo một đối tượng controller mới cho mỗi request để xử lý.

  • Vì vậy, vẫn chưa rõ chính xác vì sao vấn đề này xảy ra.

  • Bước đột phá bắt đầu xuất hiện khi họ phát hiện Unicorn, web server Rack HTTP đang dùng cho ứng dụng Rails, không tạo ra một đối tượng env riêng mới cho từng request.

  • Thay vào đó, Unicorn cấp phát một hash Ruby cho mỗi request rồi dọn sạch (clear) nó.

  • Nhờ đó, họ nhận ra log của background thread không phải là sai lệch trong hệ thống báo cáo, mà là dữ liệu request thực sự đang bị chia sẻ.

  • Họ cố tái hiện race condition này trong môi trường phát triển và phát hiện rằng để tình huống xảy ra thì phải bắt đầu từ một request ẩn danh.

  1. Khi một request ẩn danh (request #1) đi vào, một callback được đăng ký trong thư viện báo cáo ngoại lệ; callback này giữ tham chiếu tới đối tượng controller của Rails, vốn truy cập vào đối tượng môi trường request của Rack do Unicorn cung cấp.

  2. Khi ngoại lệ xảy ra trong background process, toàn bộ context được sao chép để phục vụ báo cáo, và callback cũng được đưa vào đó.

  3. Trên main thread, một request mới đã đăng nhập bắt đầu chạy. (request #2)

  4. Trên background thread, quá trình báo cáo ngoại lệ xử lý callback context. Nó cố đọc định danh session của người dùng nhưng không thấy, nên gửi request tới hệ thống xác thực thông qua controller Rails của request #1. Vì Rack dùng cùng một đối tượng cho mọi request, controller này lại tìm thấy session cookie của request #2.

  5. Trên main thread, request #2 kết thúc.

  6. Một request đã đăng nhập khác đi vào. (request #3) Việc xác thực đã hoàn tất.

  7. Trên background thread, controller ghi session cookie vào cookie jar trong môi trường Rack để hoàn tất xác thực. Lúc này đó là cookie jar dành cho request #3.

  8. Người dùng nhận response của request #3, nhưng vì session cookie của request #2 đã được ghi vào cookie jar, nên họ bị xác thực thành người dùng của request #2.

Tóm lại, nếu ngoại lệ xảy ra và quá trình xử lý các request đồng thời diễn ra đúng theo trình tự tạo ra tình huống này, thì session của một response sẽ bị thay bằng session của response trước đó. Điều này chỉ xảy ra ở header cookie; các response như HTML vẫn hoàn toàn là dữ liệu của người dùng đã được xác thực ban đầu.

Bug này chỉ xảy ra khi toàn bộ chuỗi tình huống phức tạp đó cùng hội tụ.

  • Để khắc phục, họ đã gỡ bỏ background thread mới được thêm vào và triển khai lên production ngày 5 tháng 3.

  • Sau đó, họ tạo một bản vá cho Unicorn để môi trường không còn bị chia sẻ và triển khai vào ngày 8 tháng 3.

  • Sau khi phân tích log, họ xác nhận vấn đề này chỉ xảy ra hiếm hoi, nhưng vẫn vô hiệu hóa session của toàn bộ người dùng để xử lý triệt để rủi ro tiềm ẩn.

  • Sau khi khắc phục xong, họ đã phối hợp với maintainer của Unicorn để đưa bản sửa này lên upstream.

1 bình luận

 
kunggom 2021-03-22

Xử lý song song quả thật rất phức tạp. Cuối tuần vừa rồi, để tự học, tôi cũng đã loay hoay khá lâu khi thử chạy đoạn mã mình viết gần đây theo kiểu song song bằng đúng số luồng CPU. Dù cuối cùng cũng làm được, tôi vẫn hơi lấn cấn không biết nó đã thực sự đúng hay chưa.