1 điểm bởi GN⁺ 2024-07-08 | 1 bình luận | Chia sẻ qua WhatsApp
  • @sebastianwessel/quickjs là 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à expect dự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ằng runSandboxed()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/quickjs là 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à expect dự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 variant từ @jitl/quickjs-ng-wasmfile-release-sync, và dùng loadQuickJs cùng SandboxOptions từ @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ụ SandboxOptions bao gồm các tùy chọn sau
    • allowFetch: true: inject fetch vào mã và cho phép yêu cầu dữ liệu
    • allowFs: true: mount hệ thống tệp ảo và cung cấp mô-đun node:fs
    • env: 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 join từ path và gọi join('src','dist') để in src/dist ra log của hệ thống host
  • Đọc env.MY_ENV_VAR và in "env var value" ra log của hệ thống host
  • Tạo URL bằng new URL('https://example.com'), gọi fetch(url), rồi trả về kết quả của f.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

 
GN⁺ 2024-07-08
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ư Webpack
    Cả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 fetch bằng cùng cookie với hàm fetch của host. Không nên bật fetch rồi chạy mã không đáng tin cậy
    Nế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

    • Tôi tò mò liệu dùng iframe có thể ngăn được vấn đề cookie của fetch hay không, bất kể có dùng thư viện này hay không. Hoặc muốn biết liệu nó vẫn là vấn đề ngay cả trong iframe
  • 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ì

    • Salesforce xử lý việc này bằng cách kết hợp web components với ShadowRoot đã được patch. Họ ngăn không cho đoạn mã có tham chiếu đến shadow root đi ra phần còn lại của document, và dùng hàm evaluate an toàn liên quan đến SES (Secure EcmaScript) để giới hạn các object global mà script không đáng tin cậy có thể truy cập
      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
    • Thứ gần nhất là prototype jsmirrors của Allen Wirfs-Brock, nhưng theo tôi biết nó chưa đi đến mức đặc tả cho DOM, và ngay từ đầu cũng không mấy có ý định làm vậy. Nó chỉ xử lý capability/quyền năng lực cho hệ thống lập trình JavaScript
      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ập document.createElement cho họ, mà là capability add-button. Vì vậy hãy triển khai addButton cho 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
    • Nếu muốn truy cập thật sự an toàn thì cách duy nhất là cấp cho mã bên thứ ba một iframe thuộc domain khác và thiết lập thuộc tính sandbox
      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...
    • Một cách khả thi có lẽ là thêm một API để bên thứ ba gửi HTML họ muốn render dưới dạng chuỗi, sau đó parse nó bằng một trong nhiều thư viện rồi dựng lại DOM dựa trên dữ liệu đã parse
      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
    • Nghĩ nhanh thì có thể làm kiểu này: backend request mã bên thứ ba và gắn mã đó với một chuỗi hash
      Backend sửa HTML động để chèn một thẻ div có giá trị id là chuỗi hash. Đồng thời sửa HTML để chèn một thẻ script request 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õi

Sửa mã của bên thứ ba ở backend để thay mọi instance của document.window. thành document.getElementById(hash_value)., đồng thời bắt mọi bộ chọn truy vấn phải bắt đầu bằng #hash_value
Cần thay .parentNode của prototype Element bằ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ấp
Sau đó 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 createRuntime có thể định nghĩa các lệnh gọi tới môi trường host ngoài fetch hay 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ó 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

    • Nếu thời gian khởi động tiến trình Node.js chi phối nhiều hơn thời gian chạy script, thì có thể có lợi về hiệu năng. Có lẽ khá nhiều script dạng serverless function rơi vào trường hợp này
  • 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

    • Tôi không biết cách làm bằng QJS, nhưng nếu muốn chạy bundler trong trình duyệt thì WebContainers có vẻ được tạo ra đúng cho mục đích đó
      [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 fetch mà 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

    • Tôi cũng muốn biết thêm về phần này, đặc biệt khi trộn cả service worker vào
      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 đó
    • iframe được cấp quá nhiều quyền. Ví dụ, iframe có thể rò rỉ dữ liệu cho bên thứ ba