1 điểm bởi GN⁺ 4 giờ trước | 1 bình luận | Chia sẻ qua WhatsApp
  • EEF CNA cho biết 35,8% số CVE được công bố là lỗi tiêu thụ tài nguyên không được kiểm soát, và trong hệ sinh thái BEAM, tình trạng cạn kiệt atom lặp đi lặp lại chiếm tỷ trọng lớn
  • Cạn kiệt atom là một lỗ hổng từ chối dịch vụ; atom không được garbage collection, tích lũy trong bảng toàn cục, và khi bảng đầy thì VM sẽ crash
  • Nếu tạo atom từ dữ liệu không thể bảo đảm có tập giá trị hữu hạn, như đầu vào người dùng, sẽ phát sinh rủi ro DoS; ngay cả URI scheme cũng không ngoại lệ
  • Rủi ro tồn tại không chỉ ở các lời gọi tường minh như binary_to_atom/1, String.to_atom/1, mà còn ở giải mã khóa JSON thành atom và tạo động dựa trên nội suy chuỗi
  • Cách xử lý an toàn là tránh tạo atom mới ở runtime, giới hạn giá trị đã biết bằng bảng tra cứu tường minh hoặc nhóm hàm to_existing_atom, và kiểm tra bằng linter

Lỗ hổng từ chối dịch vụ do cạn kiệt atom gây ra

  • Trong các CVE do EEF CNA công bố, 35,8% là lỗi tiêu thụ tài nguyên không được kiểm soát, và trong hệ sinh thái BEAM, vấn đề cạn kiệt atom lặp đi lặp lại chiếm tỷ trọng lớn {p:36}
  • Có thể xem phân bố hiện tại trên trang Common Weaknesses của EEF CNA
  • Cạn kiệt atom là một lỗ hổng từ chối dịch vụ (DoS)
    • Atom không được garbage collection
    • Được lưu trong bảng atom toàn cục
    • Khi bảng đầy, VM sẽ crash
  • Tạo atom từ các giá trị không hữu hạn, đặc biệt là đầu vào người dùng, có thể dẫn đến DoS tiềm ẩn
  • Rủi ro không chỉ giới hạn ở những lời gọi hiển nhiên
    • Erlang: binary_to_atom/1, list_to_atom/1
    • Elixir: String.to_atom/1, List.to_atom/1
  • Cũng có những mẫu rủi ro ít dễ nhận ra hơn
    • Tạo atom động bằng nội suy trong Erlang:
      % Erlang: 보간을 통한 동적 atom 생성
      list_to_atom("field_" ++ UserInput)
      
    • Giải mã khóa JSON thành atom trong Elixir:
      
      
      
      # Elixir: JSON을 atom 키로 디코딩
      Jason.decode(json, keys: :atoms)
      
    • Tạo atom động bằng nội suy trong Elixir:
      
      
      
      # Elixir: 보간을 통한 동적 atom 생성
      :"field_#{user_input}"
      

Cách xử lý an toàn và các điểm cần kiểm tra

  • Lỗ hổng cạn kiệt atom không đơn thuần là do bất cẩn, mà thường xuất hiện trong những đoạn mã giả định rằng đầu vào được kiểm soát hoặc là hữu hạn
  • URI scheme là một ví dụ tiêu biểu
    • Có thể dễ nghĩ rằng chỉ có vài scheme cần xử lý
    • Nhưng nếu giá trị đến từ đầu vào bên ngoài, thì không còn bảo đảm rằng tập giá trị là hữu hạn nữa
  • Mã tạo atom từ đầu vào là không an toàn, trừ khi tập giá trị có thể có là hữu hạn, đã biết và được ép buộc
  • Cách tiếp cận an toàn nhất là không tạo atom mới ở runtime
  • Nếu các giá trị được phép đã biết trước, dùng bảng tra cứu tường minh sẽ an toàn hơn
    % Erlang
    case Scheme of
        <<"http">> -> http;
        <<"https">> -> https;
        _ -> error
    end
    
  • Khi bảng tra cứu không thực tế, nên dùng các biến thể chỉ sử dụng atom đã tồn tại thay vì tạo atom mới
    • Các hàm này không tạo atom mới mà sẽ phát sinh lỗi
    % Erlang
    binary_to_existing_atom(Value)
    list_to_existing_atom(Value)
    
    
    
    
    # Elixir
    String.to_existing_atom(value)
    List.to_existing_atom(value)
    
  • Linter giúp phát hiện các mẫu rủi ro trước khi chúng trở thành lỗ hổng
    • Với dự án Elixir, có thể cân nhắc bật Credo.Check.Warning.UnsafeToAtom của Credo
    • Kiểm tra này sẽ đánh dấu các lời gọi không an toàn dùng String.to_atom/1, List.to_atom/1, Module.concat/1,2Jason.decode/2 với keys: :atoms
    • Kiểm tra này bị tắt theo mặc định
  • Người bảo trì các dự án Erlang hoặc Elixir nên tìm kiếm những đoạn mã tạo atom từ binary, chuỗi, khóa JSON, thành phần URI, header và giá trị cấu hình
  • Đây là một nhóm lỗ hổng thuộc loại dễ sửa trước khi trở thành CVE
  • Hướng dẫn chi tiết hơn được tổng hợp trong hướng dẫn phòng tránh cạn kiệt atom của EEF Security Working Group

1 bình luận

 
Ý kiến trên Lobste.rs
  • Nghe giống với tình huống của Symbol trong Ruby trước khi nó trở thành đối tượng được garbage collection

  • Không hiểu tiêu đề. Cái này rõ ràng trông như một footgun

    • Có vẻ ý của tiêu đề là nếu gọi cạn kiệt atom là “chỉ là footgun” thì sẽ đánh giá thấp mức độ nghiêm trọng của vấn đề
    • Nếu nhớ không nhầm, dù tôi không dùng Erlang hằng ngày, thì atom không được garbage collection
      Nếu nghĩ “Ruby cũng có symbol giống atom của Erlang mà?” thì đúng, nhưng Ruby có garbage collection cho symbol
      Hơn nữa, bảng tra cứu nơi atom Erlang được lưu theo mặc định chỉ cho phép tối đa 1.048.576 mục
      Nếu tạo atom động từ đầu vào người dùng như form thì rất nguy hiểm, và phần mềm sẽ phơi lộ trước tấn công từ chối dịch vụ
    • Tôi hiểu ý là đây là vấn đề lớn hơn một footgun “đơn thuần”
      Tuy vậy theo kinh nghiệm của tôi thì bản thân từ “footgun” đã là một cách nói khá rộng, nên dù theo cách nào thì câu chữ trong tiêu đề vẫn hơi gượng
    • Đúng vậy, mà còn là một footgun cực kỳ lớn nữa
  • Khá bất ngờ vì nghe như có gì đó thiết kế hoặc triển khai cốt lõi bị tệ. Càng lạ hơn vì đây là ngôn ngữ vốn luôn được khen trên Internet

    • Atom trong BEAM về bản chất là chuỗi được intern, và có một bảng toàn cục byte↔số nguyên
      Nếu thêm đếm tham chiếu vào bảng đó thì chi phí sẽ lớn, và đặc tính mở rộng của lượng lớn mã đã tồn tại hàng chục năm sẽ thay đổi
      Số lượng atom tối đa mặc định là 1 triệu và được quyết định khi VM khởi động
      Đây đúng là một cái bẫy, nhưng không khó tránh. Khuyến nghị từ rất lâu rồi là “đừng tạo atom từ đầu vào người dùng”
      Ví dụ nếu phân tích JSON thì thường либо không chuyển key thành atom, либо chỉ chuyển khi đó là atom đã tồn tại. Làm vậy vẫn có thể pattern match bằng key atom, các atom đó đã được tạo sẵn khi nạp mã, còn nhánh tổng quát thì có thể nhận chuỗi thay vì atom
    • Cũng cần tính đến việc Erlang từng là một ngôn ngữ ngách do các lập trình viên chuyên nghiệp dùng cho trường hợp đặc thù
      Elixir thì phổ biến đại chúng hơn nhiều, nên lập trình viên Erlang có khả năng biết điều này còn lập trình viên Elixir thì có thể không
  • Cá nhân tôi thấy việc dùng atom theo kiểu đó vốn đã hơi lạ. Vì tôi hiểu atom trong Erlang gần giống kiểu enum trong C
    Tôi xem nó như một tính năng tiện lợi: nhập từ theo một cách nhất định thì bên trong sẽ thành enum
    Bài viết có nhắc đến đầu vào người dùng, nhưng ngay từ đầu tôi cũng không hiểu tại sao lại có use case muốn tạo kiểu enum mới từ đầu vào người dùng. Trông như phạm vi sử dụng cực hẹp
    Các bình luận bên cạnh nói về parsing, nhưng lý tưởng thì chẳng phải là đang phân tích dữ liệu có cấu trúc đã biết trước sao? Tôi có cảm giác mình đang bỏ sót điều gì đó

    • Hơi lệch khỏi câu hỏi một chút, nhưng trong K, symbol được intern toàn cục, và giống Erlang, bạn có thể làm cạn bảng symbol trong một tiến trình K rồi làm nó chết
      Trong ngôn ngữ đó, việc có symbol như một kiểu riêng chứ không chỉ để so sánh bằng nhanh hơn có ít nhất hai lợi điểm. Symbol là atom, tức một đơn vị nguyên tử chứ không phải dãy dạng list của các ký tự, nên nhiều toán tử xử lý nó khác đi; và symbol còn được vector hóa để lưu dày đặc trong danh sách đơn kiểu
      Trong K và Q, việc biểu diễn các cột của bảng cơ sở dữ liệu bằng kiểu vector hóa là rất đáng mong muốn. Nó có locality tốt, dùng bộ nhớ hiệu quả hơn và nhiều toán tử có đường chạy nhanh. Nhưng do ràng buộc của bảng symbol, cần cẩn thận khi dùng symbol cho các cột có cardinality cao
      Nếu phân tích JSON với schema đã biết thì symbol rất phù hợp làm key từ điển, và trong k2/k3 gần như là bắt buộc. Nhưng nếu là JSON không rõ nguồn gốc thì không nên lấy từ đầu vào người dùng
      Một số phương ngữ K giới hạn độ dài symbol ở mức ngắn để có thể đóng gói và di chuyển nó dưới dạng giá trị 64-bit. Đổi lại là mất tính tổng quát, nhưng không còn cần bảng symbol nữa
  • Sự phân biệt giữa “đầu vào được kiểm soát” và “đầu vào không được kiểm soát” trong bảo mật nghe giống như chuyện có null hay không vậy
    Khi thấy các mục kiểu webpack-plugin-less-css sẽ bị từ chối dịch vụ nếu nhận file CSS không đáng tin cậy, tôi khá mệt mỏi với CVE
    Dù vậy, ở đây sẽ tốt hơn nếu có cách đánh dấu ranh giới rõ hơn. Ví dụ, nếu có thể xử lý tốt cả các quy tắc kết hợp như thuộc tính an toàn nào còn được giữ lại sau khi nối chuỗi thì hay hơn
    Và nếu bạn lấy cả đống thứ nhận từ HTTP POST rồi đánh dấu thành SafeString, thì điều đó ở mức nào đó là trách nhiệm của chính bạn