- 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,2 và Jason.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
Symboltrong Ruby trước khi nó trở thành đối tượng được garbage collectionKhông hiểu tiêu đề. Cái này rõ ràng trông như một footgun
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ụ
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
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
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
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ì đó
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ó
nullhay không vậyKhi thấy các mục kiểu
webpack-plugin-less-csssẽ 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 CVEDù 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