HN ra mắt: Chạy JavaScript trong sandbox WebAssembly QuickJS
(github.com/sebastianwessel)@sebastianwessel/quickjslà một gói TypeScript cho phép chạy an toàn mã JavaScript và TypeScript trong một sandbox được biên dịch từ engine QuickJS sang WebAssembly- Tập trung vào việc cô lập và chạy mã không đáng tin cậy, cung cấp môi trường thực thi nhẹ và nhanh thông qua sandbox QuickJS WebAssembly
- Hỗ trợ các mô-đun Node.js cơ bản, mount hệ thống tệp ảo, mount mô-đun Node tùy chỉnh, cung cấp client
fetch, kèm test runner vàexpectdựa trên Chai - Ví dụ sử dụng: nạp QuickJS wasm một lần bằng
loadQuickJs(), sau đó chạy mã bằngrunSandboxed()vàevalCode(), đồng thời truyền các tùy chọn nhưallowFetch,allowFs,env - Các nhà phát triển cần chạy mã JavaScript bên ngoài trong ứng dụng TypeScript có thể dùng nó như một môi trường thực thi cân bằng giữa cô lập, hiệu năng và sự đơn giản của API
Gói sandbox QuickJS WebAssembly
@sebastianwessel/quickjslà một gói TypeScript cho phép chạy mã JavaScript và TypeScript trong sandbox WebAssembly- Gói này sử dụng engine QuickJS được biên dịch sang WebAssembly, phù hợp cho mục đích chạy mã không đáng tin cậy trong môi trường cô lập
- Gói tận dụng engine QuickJS nhẹ và nhanh để cung cấp một môi trường vững chắc cho việc thực thi mã
- Cung cấp tài liệu đầy đủ, ví dụ và playground trực tuyến
Tính năng cung cấp
- Thực thi an toàn: chạy mã JavaScript và TypeScript không đáng tin cậy trong môi trường an toàn và cô lập
- Mô-đun Node.js cơ bản: cung cấp hỗ trợ cho các mô-đun Node.js chuẩn cơ bản phục vụ các trường hợp sử dụng phổ biến
- Hệ thống tệp: có thể mount hệ thống tệp ảo
- Mô-đun Node tùy chỉnh: có thể mount các mô-đun Node do người dùng định nghĩa
- Gọi mạng: cung cấp client
fetchđể thực hiện các lời gọi http(s) - Hỗ trợ kiểm thử: bao gồm test runner và
expectdựa trên Chai - Hiệu năng và khả năng tích hợp: tận dụng sự nhẹ và hiệu quả của engine QuickJS, đồng thời dễ tích hợp vào các dự án TypeScript hiện có
- API đơn giản: cung cấp API thân thiện để chạy và quản lý mã JavaScript và TypeScript trong sandbox
Luồng sử dụng cơ bản
- Trước tiên import
varianttừ@jitl/quickjs-ng-wasmfile-release-sync, và dùngloadQuickJscùngSandboxOptionstừ@sebastianwessel/quickjs loadQuickJs(variant)nạp và khởi tạo QuickJS wasm; thao tác này tốn nhiều tài nguyên, nên được khuyến nghị thực hiện chỉ một lần nếu có thểloadQuickJs()trả vềrunSandboxed, được dùng cho các lần thực thi trong sandbox sau đó- Ví dụ
SandboxOptionsbao gồm các tùy chọn sauallowFetch: true: injectfetchvào mã và cho phép yêu cầu dữ liệuallowFs: true: mount hệ thống tệp ảo và cung cấp mô-đunnode:fsenv: truyền các giá trị biến môi trường để mã trong sandbox sử dụng
Các hành vi có thể có trong ví dụ thực thi
- Mã trong sandbox import
jointừpathvà gọijoin('src','dist')để insrc/distra log của hệ thống host - Đọc
env.MY_ENV_VARvà in"env var value"ra log của hệ thống host - Tạo URL bằng
new URL('https://example.com'), gọifetch(url), rồi trả về kết quả củaf.text() - Chạy mã theo dạng
runSandboxed(async ({ evalCode }) => evalCode(code), options) - Ví dụ kết quả có dạng
{ ok: true, data: '<!doctype html>\n<html>\n[....]</html>\n' }
Giấy phép và trường hợp sử dụng phù hợp
- Dự án này được cung cấp theo MIT License
- Phù hợp với các nhà phát triển muốn chạy mã JavaScript an toàn bên trong ứng dụng TypeScript
- Cung cấp môi trường thực thi đảm bảo cả hiệu năng và an toàn thông qua sandbox QuickJS WebAssembly
1 bình luận
Các ý kiến trên Hacker News
Với tư cách là tác giả của thư viện runtime quickjs-emscripten, tôi thích phong cách thư viện chuẩn mang tính công thái học mà bạn đã tạo cho quickjs-emscripten
Tôi tò mò liệu bạn đã thử chạy nó trong trình duyệt hoặc bundler chưa. Cách nhận tên biến thể dưới dạng chuỗi rồi truyền động vào
import(variantName)có thể không tương thích tốt với các công cụ như WebpackCảnh báo bảo mật: Thư viện này cho phép mã không đáng tin cậy của guest gọi
fetchbằng cùng cookie với hàmfetchcủa host. Không nên bậtfetchrồi chạy mã không đáng tin cậyNếu mã bên trong sandbox có thể gọi tùy ý các HTTP API đã được xác thực trong ngữ cảnh host, thì đó không phải là “sandbox”
Lý do quickjs-emscripten ở mức thấp và tránh các tính năng kiểu ma thuật là để có thể tự tin nói rằng API mà nó cung cấp là an toàn. Tôi thường từ chối các yêu cầu về serialization kiểu ma thuật hoặc khả năng truy cập mạng/hệ thống tệp dễ dàng, vì những đoạn mã như vậy là khu vực rất dễ xảy ra sai sót bảo mật
Khi chạy mã không đáng tin cậy, bạn phải kiểm toán cẩn thận không chỉ bản thân sandbox mà cả mã bạn viết cho các API được phơi ra cho sandbox
Tôi nhận ra vấn đề bảo mật tiềm ẩn sau khi thấy một bình luận của người dùng HN khác hỏi về cookie của Fetch
Đọc thêm: bài viết về bảo mật sandbox plugin của Figma https://www.figma.com/blog/how-we-built-the-figma-plugin-sys..., https://www.figma.com/blog/an-update-on-plugin-security/, README của Quickjs-emscripten https://github.com/justjake/quickjs-emscripten
Có nhiều cách để sandbox JavaScript cả phía server lẫn phía trình duyệt, nhưng tôi không biết có cách nào để “sandbox” quyền truy cập DOM hay không
Ví dụ như chỉ cho bên thứ ba không đáng tin cậy truy cập vào các phần tử DOM ở vị trí đã định trước. Theo tôi biết, công nghệ duy nhất cho phép việc này là iframe, nhưng tiếc là nó nặng và chậm
Tôi đang làm một ứng dụng có thể host plugin, và nếu cấp quyền truy cập DOM cho plugin thì có vẻ như chúng có thể làm đúng nghĩa là bất cứ thứ gì
Hàm evaluate an toàn đó khá ấn tượng. Phần cốt lõi có lẽ ở đây: https://github.com/Agoric/realms-shim/blob/v1.1.0/src/evalua...
Cũng có ý tưởng web component được cô lập để giải quyết vấn đề này ở cấp nền tảng: https://github.com/WICG/webcomponents/issues/1002
Lấy cảm hứng từ jsmirrors, bạn có thể tự thử làm thứ gì đó như “dommirrors”, nhưng đó là một việc lớn. Cũng có cách đi vòng bằng cách dùng nguyên jsmirrors để đạt được phần nào điều bạn muốn, nhưng không dễ dùng
Tuy nhiên, việc trung gian hóa hoặc mô phỏng quyền truy cập DOM để cấp cho họ gần như chắc chắn không phải là điều bạn thật sự muốn. Nên xác định đối phương thực sự được phép làm gì, rồi chỉ cấp capability cho phép hành vi đó
Ví dụ, nếu muốn cho họ thêm một nút ở đâu đó, bạn có thể nghĩ rằng phải đưa cho họ một điểm neo là phần tử cha và để họ tạo node DOM bằng
document.createElement. Nhưng thứ bạn thực sự muốn không phải là cấp quyền truy cậpdocument.createElementcho họ, mà là capability add-button. Vì vậy hãy triển khaiaddButtoncho họXem thêm: <https://news.ycombinator.com/item?id=30703531#30706060>
Tốt nhất đừng nghe những người nói CSP dùng cho mục đích này. Không phải vậy. Nói chính xác thì CSP được thiết kế tệ và thù địch với người dùng ngay cả trong mục tiêu ban đầu của nó, và theo tôi lẽ ra nó không nên được triển khai và mở rộng như hiện nay. Dựa vào nó là nguy hiểm
Trang cha có thể đo kích thước nội dung DOM và điều chỉnh kích thước iframe trong một giới hạn nhất định, nên vẫn có thể tích hợp tự nhiên hơn vào UI của ứng dụng
Tùy theo mức độ hiển thị giữa người dùng và mức độ tin cậy, cần cẩn thận với các nỗ lực giả mạo/phishing và clickjacking bên trong iframe. Lý tưởng nhất là khóa frame để nó hoàn toàn không thể thực hiện request web, đồng nghĩa với việc cũng không tải được hình ảnh
Khi đó, chẳng hạn ngay cả nếu người dùng bị lừa nhập mật khẩu vào một form mật khẩu giả, cũng không có cách nào rò rỉ dữ liệu ra ngoài frame
Cách chính để hạn chế các loại tài nguyên mà iframe có thể request là Content-Security-Policy, qua đó có thể tắt toàn bộ ảnh, script bên thứ ba, v.v.
https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameE...
https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
Cũng nên bật các thuộc tính sandbox khác, và tắt quyền truy cập các DOM API nhạy cảm về quyền riêng tư như webcam
https://developer.mozilla.org/en-US/docs/Web/HTML/Element/if...
https://developer.mozilla.org/en-US/docs/Web/Security/IFrame...
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Pe...
Như vậy có thể giới hạn bằng whitelist các phần tử và thuộc tính HTML được phép. Nếu muốn cho phép cả việc lắng nghe sự kiện DOM native thì sẽ phức tạp hơn, nhưng không phải là không thể
Sẽ cần một API nhận chuỗi tên sự kiện và id của phần tử nhận sự kiện; sau đó phát hiện sự kiện đó trên DOM thật rồi sao chép cùng sự kiện vào trong sandbox JavaScript đang dùng. Trong sandbox đó phải có quyền truy cập vào API nói trên
Backend sửa HTML động để chèn một thẻ
divcó giá trị id là chuỗi hash. Đồng thời sửa HTML để chèn một thẻscriptrequest mã bên thứ ba từ domain của chính mình, và thêm giá trị hash vào thuộc tính data của thẻscriptđó để theo dõiSửa mã của bên thứ ba ở backend để thay mọi instance của
document.vàwindow.thànhdocument.getElementById(hash_value)., đồng thời bắt mọi bộ chọn truy vấn phải bắt đầu bằng#hash_valueCần thay
.parentNodecủa prototypeElementbằng một thuộc tính tùy chỉnh, để kiểm tra và chặn các nỗ lực thoát khỏi container được cung cấpSau đó gửi tài liệu HTML đến trình duyệt. Mã của bên thứ ba bị hỏng cũng không sao. Các ràng buộc phải được thông báo cho bên thứ ba, và họ phải tự kiểm thử mã của mình trước khi gửi lên máy chủ
Điều quan trọng chỉ là không để mã thoát khỏi container được cung cấp động. Phần này cần được kiểm thử định kỳ để tìm các vi phạm bảo mật
Thực tế có thể không chạy được, nhưng thử nghiệm thì có vẻ thú vị
Tình cờ tuần trước tôi đã thử QuickJS, và cuối cùng chốt dùng isolated-vm. Cả hai đều đáp ứng các cam kết về bảo mật, nhưng isolated-vm có hiệu năng tốt hơn nhiều về chi phí thiết lập, hủy bỏ và chạy
eval.Đây là một cách tiếp cận thú vị. Với tư cách là tác giả của một thư viện sandbox JavaScript khác[1], dùng worker để cô lập và các kỹ thuật dọn dẹp môi trường JavaScript, tôi cho rằng cách diễn giải JavaScript—tức JavaScript trong JavaScript, hoặc như trường hợp này là JavaScript trong WASM—mang lại mức cô lập cao nhất
Ngoài ra, nó cũng không trực tiếp phơi nhiễm trước các lỗi của chính máy ảo JavaScript phía host. Nếu nhắm tới Node thì điều này có thể còn quan trọng hơn, vì ngoài một số xu hướng gần đây, Node.js dường như không được thiết kế với cô lập và sandboxing trong đầu, khác với Deno
Nhìn API thì tôi không thấy
createRuntimecó thể định nghĩa các lệnh gọi tới môi trường host ngoàifetchhay không. Tính năng này hẳn sẽ khá hữu ích, vì có thể hạn chế giao tiếp với thế giới bên ngoài theo cách được kiểm soát, thay vì hoặc cho phép tất cả hoặc chặn tất cảTương tự, có vẻ cũng không hỗ trợ trình duyệt. Ít nhất là khi tôi kiểm tra nhanh bằng esm.sh thì là vậy. Đây cũng có thể là một tính năng hữu ích
Tôi tò mò overhead ở mức nào nên sẽ thử kiểm tra, và như đã nói ở trên, bản thân cách tiếp cận này trông khá vững chắc
[1] @exact-realty/lot
Cũng có API để expose hàm host, gọi hàm guest, custom module loader, v.v.: https://github.com/justjake/quickjs-emscripten?tab=readme-ov...
Tài liệu API
newFunction: https://github.com/justjake/quickjs-emscripten/blob/main/doc...Có vẻ ý là CPU đã nhanh đến mức giờ ta chạy interpreter bên trong interpreter.
Tôi sẽ không nêu hiệu năng như một lợi thế của việc chạy JavaScript trong QuickJS. QuickJS hoàn toàn không cạnh tranh được với máy ảo JavaScript của host
Tuy nhiên, nó có thể nhanh hơn các interpreter C cũ hoặc interpreter viết bằng JavaScript
Có vẻ có thể dùng cái này để chạy mã JavaScript do người dùng cung cấp. Tôi đang tìm cách bundle mã TypeScript của người dùng trong môi trường sandbox
Không biết có khuyến nghị nào về cách chạy một bundler như webpack bên trong QuickJS không
[1]: https://webcontainers.io/
Rất hay. Nếu đã được compile sang WASM thì tôi tò mò liệu có thể chạy trong trình duyệt không. Nếu được, và có thể thực hiện request
fetchmà không gắn cookie vào request, thì sẽ khá thú vịỞ công ty trước, dự án đã bị tạm dừng vì gặp quá nhiều segmentation fault và lỗi im lặng trong Quickjs-emscripten
Nếu phải làm lại, có lẽ tôi sẽ dùng một engine hoạt động đúng với nhiều chương trình hơn và có bundle WASM được phê duyệt chính thức
Tôi từng nghĩ có thể sandbox mã một cách an toàn bằng iframe, nhưng không chắc. Dĩ nhiên nếu dùng interpreter riêng thì có vẻ có thể đưa vào nhiều tính năng hơn như giới hạn thời gian chặt chẽ hơn, API tùy chỉnh
Hiện tôi đang dùng iframe nhận mã qua window message; sau khi đánh giá mã đầu vào, nó có thể trả phản hồi lại bằng window message nên hoạt động khá tốt. Tuy nhiên tôi không chắc có lỗ hổng nào để thoát khỏi sandbox không
Với trường hợp phức tạp hơn, chẳng hạn phụ thuộc package, tôi đã định parse bằng Babel rồi tạo import map dùng CDN (esm.sh), nhưng có lúc phiên bản đã đưa lên CDN không hoạt động tốt
Vì vậy tôi dùng StackBlitz, và nó hoạt động khá tốt, nhưng có một số vấn đề trong môi trường không phải security context
Cuối cùng tôi tạo một web server nhỏ nhận mã và dependency (package.json), thực hiện build Vite trong Docker container rồi trả kết quả về. Nó hoạt động tương đối ổn nhưng đôi khi chậm
Sẽ rất tốt nếu có thể build hoàn toàn ở phía client, và StackBlitz về cơ bản đang làm việc kiểu đó