Một nền tảng học tập cho phép học sinh soạn các truy vấn MongoDB đã loại bỏ module vm của Node.js và hiện đang phân tích mọi truy vấn thành một cây cú pháp trừu tượng (AST) trước khi thực thi. Thay đổi này loại bỏ một sandbox (môi trường cô lập) vốn có thể bị phá vỡ, giúp bảo vệ backend khỏi các mã tùy ý mà một chuỗi định dạng sai có thể chèn vào.
Tại sao sandbox ban đầu thất bại
Bản triển khai đầu tiên đã bao bọc một handle cơ sở dữ liệu đang hoạt động trong một lời gọi vm.runInContext và cố gắng hạn chế người dùng bằng biểu thức chính quy (regular expression). Nó chỉ cho phép các tên phương thức như find hoặc aggregate; mọi định danh khác đều được cho là sẽ bị lọc bỏ.
Hai lỗ hổng đã khiến cách tiếp cận đó không an toàn:
- Lọc bằng regex có thể bị vượt qua. JavaScript cho phép mã sử dụng ký hiệu ngoặc vuông (
obj["constructor"]) để truy cập bất kỳ thuộc tính nào. Kẻ tấn công có thể lấy được constructorFunction, xây dựng một hàm mới và chạy bất kỳ mã nào chúng muốn. Regex không bao giờ nhìn thấy việc truy cập thuộc tính thực sự vì nguồn mã có thể được viết lại theo vô số cách. vmkhông phải là một ranh giới bảo mật. Tài liệu của Node cho biếtvmcô lập đối tượng global nhưng không cô lập toàn bộ tiến trình (process). Bằng cách chèn một kết nối cơ sở dữ liệu đang hoạt động vào sandbox, mã bên trong ngữ cảnh vẫn giữ được khả năng gọi bất kỳ phương thức nào trên kết nối đó, bao gồm cả những phương thức ghi hoặc xóa dữ liệu. Sandbox đã không ngăn được mã ảnh hưởng đến tiến trình máy chủ (host process).
Giải pháp dựa trên AST
Nhóm phát triển đã thay thế việc thực thi mã bằng phân tích tĩnh. Các chuỗi truy vấn hiện được đưa vào bộ phân tích acorn, bộ phân tích này tạo ra một AST—một biểu diễn dạng cây của cấu trúc cú pháp của mã. AST được kiểm tra từng nút (node) một dựa trên một danh sách trắng (whitelist) nghiêm ngặt:
- Các giá trị literal, mảng và đối tượng chỉ được phép khi chúng xuất hiện dưới dạng các giá trị thuần túy.
- Các lời gọi phương thức được giới hạn trong một tập hợp đã được định nghĩa trước (
find,sort,limit, v.v.). Bất kỳ lời gọi nào khác đều bị từ chối. - Truy cập thuộc tính tính toán (ví dụ:
obj[expr]) hoặc bất kỳ loại nút nào không được liệt kê rõ ràng sẽ kích hoạt lỗi ngay lập tức.
Vì bộ phân tích hoạt động trên cây chứ không phải trên văn bản thô, nó không thể bị đánh lừa bởi các cách viết khác nhau hoặc các mẹo sử dụng ký hiệu ngoặc vuông. Một chuỗi constructor vốn có thể lọt qua regex sẽ xuất hiện dưới dạng một nút không xác định và bị từ chối trước khi bất kỳ mã nào được chạy.
Điều này có ý nghĩa gì đối với bảo mật
Thiết kế mới tuân theo triết lý “từ chối theo mặc định” (deny by default):
- Xác định những gì được phép, thay vì những gì bị cấm. Việc lập danh sách cho phép bằng văn bản không thể bao quát hết mọi trường hợp; một AST có một tập hợp hữu hạn các loại nút, giúp việc kiểm tra toàn diện trở nên khả thi.
- Không bao giờ để lộ các tài nguyên đang hoạt động bên trong sandbox. Việc truyền một handle cơ sở dữ liệu vào một ngữ cảnh cô lập sẽ cung cấp cho mã trong sandbox một đường dẫn trực tiếp đến backend. Cách tiếp cận bằng bộ phân tích không bao giờ đưa một đối tượng đang hoạt động cho mã người dùng; nó chỉ trích xuất ý định của truy vấn.
- Xác thực cấu trúc, sau đó thực thi một cách an toàn. Sau khi AST vượt qua bước xác thực, nền tảng sẽ chuyển đổi các lời gọi được phép thành các phương thức driver MongoDB thực tế bằng cách sử dụng đường dẫn mã đáng tin cậy của riêng nó.
Những điều cần lưu ý tiếp theo
- Kiểm tra bất kỳ việc sử dụng
eval,new Function, hoặcvmnào trong mã nguồn của bạn. Ngay cả một danh sách trắng cũng có thể bị vô hiệu hóa bởi tính chất động của JavaScript. - Áp dụng phân tích AST cho mã do người dùng tạo bất cứ khi nào có thể. Các thư viện như
acorn,esprima, hoặcbabel-parsergiúp việc chuyển đổi này trở nên đơn giản. - Hạn chế việc để lộ các đối tượng đang hoạt động. Nếu một kết nối cơ sở dữ liệu, handle tệp, hoặc socket mạng phải có khả năng truy cập được, hãy bao bọc nó trong một proxy chỉ hiển thị các phương thức tối thiểu mà bạn dự định cho phép.
- Tự động hóa việc kiểm tra các trường hợp biên (edge cases). Tạo các truy vấn sử dụng ký hiệu ngoặc vuông, các khóa tính toán (computed keys), hoặc thao tác prototype để xác minh rằng bộ phân tích của bạn từ chối chúng.
Bài học rút ra
Việc dựa vào vm.runInContext như một sandbox tạo ra một cảm giác an toàn giả tạo; các bộ lọc regex không thể bao quát hết cú pháp linh hoạt của JavaScript, và sandbox không cô lập được các tài nguyên của tiến trình. Việc phân tích đầu vào của người dùng thành một AST và chỉ đưa vào danh sách trắng các nút mà bạn hiểu sẽ cung cấp một rào cản cụ thể, dễ bảo trì, giúp ngăn chặn mã độc trước khi nó được thực thi. Nếu nền tảng của bạn cho phép người dùng viết mã, hãy thay thế việc thực thi kiểu eval bằng phân tích tĩnh ngay hôm nay.
