1P by GN⁺ | ★ favorite | 댓글 1개
  • @sebastianwessel/quickjs는 QuickJS 엔진을 WebAssembly로 컴파일한 샌드박스 안에서 JavaScript와 TypeScript 코드를 안전하게 실행하는 TypeScript 패키지임
  • 신뢰할 수 없는 코드를 격리해 실행하는 데 초점을 두며, QuickJS WebAssembly 샌드박스를 통해 가볍고 빠른 실행 환경을 제공함
  • 기본 Node.js 모듈 지원, 가상 파일 시스템 마운트, 커스텀 Node 모듈 마운트, fetch 클라이언트 제공, 테스트 러너와 Chai 기반 expect를 포함함
  • 사용 예시는 loadQuickJs()로 QuickJS wasm을 한 번 로드한 뒤 runSandboxed()evalCode()로 코드를 실행하고, allowFetch, allowFs, env 같은 옵션을 전달함
  • TypeScript 애플리케이션에서 외부 JavaScript 코드를 실행해야 하는 개발자는 격리, 성능, API 단순성을 함께 고려한 실행 환경으로 활용할 수 있음

QuickJS WebAssembly 샌드박스 패키지

  • @sebastianwessel/quickjsJavaScript와 TypeScript 코드를 WebAssembly 샌드박스 안에서 실행할 수 있게 하는 TypeScript 패키지임
  • QuickJS 엔진을 WebAssembly로 컴파일해 사용하며, 신뢰할 수 없는 코드를 격리된 환경에서 실행하는 용도에 맞춰져 있음
  • 패키지는 가볍고 빠른 QuickJS 엔진을 활용해 코드 실행을 위한 견고한 환경을 제공함
  • 전체 문서, 예제, 온라인 플레이그라운드를 제공함

제공 기능

  • 보안 실행: 신뢰할 수 없는 JavaScript와 TypeScript 코드를 안전하고 격리된 환경에서 실행함
  • 기본 Node.js 모듈: 일반적인 사용 사례를 위한 기본 표준 Node.js 모듈 지원을 제공함
  • 파일 시스템: 가상 파일 시스템을 마운트할 수 있음
  • 커스텀 Node 모듈: 사용자 정의 Node 모듈을 마운트할 수 있음
  • 네트워크 호출: fetch 클라이언트를 제공해 http(s) 호출을 수행할 수 있음
  • 테스트 지원: 테스트 러너와 Chai 기반 expect를 포함함
  • 성능과 통합성: QuickJS 엔진의 가벼움과 효율성을 활용하며 기존 TypeScript 프로젝트에 쉽게 통합할 수 있음
  • 단순한 API: 샌드박스 안에서 JavaScript와 TypeScript 코드를 실행하고 관리하기 위한 사용자 친화적 API를 제공함

기본 사용 흐름

  • 먼저 @jitl/quickjs-ng-wasmfile-release-sync에서 variant를 가져오고, @sebastianwessel/quickjs에서 loadQuickJsSandboxOptions를 사용함
  • loadQuickJs(variant)는 QuickJS wasm을 로드하고 초기화하며, 이 작업은 자원 집약적이므로 가능하면 한 번만 수행하는 방식이 권장됨
  • loadQuickJs()runSandboxed를 반환하며, 이후 샌드박스 실행에 사용됨
  • SandboxOptions 예시는 다음 옵션을 포함함
    • allowFetch: true: 코드에 fetch를 주입하고 데이터 요청을 허용함
    • allowFs: true: 가상 파일 시스템을 마운트하고 node:fs 모듈을 제공함
    • env: 샌드박스 코드에서 사용할 환경 변수 값을 전달함

실행 예시에서 가능한 동작

  • 샌드박스 안의 코드는 path에서 join을 import하고 join('src','dist')를 호출해 호스트 시스템 로그에 src/dist를 출력함
  • env.MY_ENV_VAR를 읽어 호스트 시스템 로그에 "env var value"를 출력함
  • new URL('https://example.com')로 URL을 만들고 fetch(url)을 호출한 뒤 f.text() 결과를 반환함
  • runSandboxed(async ({ evalCode }) => evalCode(code), options) 형태로 코드를 실행함
  • 결과 예시는 { ok: true, data: '<!doctype html>\n<html>\n[....]</html>\n' } 형태임

라이선스와 적합한 사용처

  • 이 프로젝트는 MIT License로 제공됨
  • TypeScript 애플리케이션 안에서 JavaScript 코드를 안전하게 실행하려는 개발자에게 적합함
  • QuickJS WebAssembly 샌드박스를 통해 성능과 안전성을 함께 확보하는 실행 환경을 제공함

댓글과 토론

Hacker News 의견들
  • quickjs-emscripten 런타임 라이브러리 작성자로서, quickjs-emscripten용으로 만든 인체공학적인 표준 라이브러리 스타일이 마음에 듦
    브라우저나 번들러에서 실행해 봤는지 궁금함. 변형 이름을 문자열로 받아 import(variantName)에 동적으로 넘기는 방식은 Webpack 같은 도구와 잘 맞지 않을 수 있음
    보안 경고: 이 라이브러리는 게스트의 신뢰할 수 없는 코드가 호스트 fetch 함수와 같은 쿠키로 fetch를 호출할 수 있게 함. fetch를 켜고 신뢰할 수 없는 코드를 실행하면 안 됨
    샌드박스 안의 코드가 호스트 컨텍스트로 인증된 임의의 HTTP API를 호출할 수 있다면 그건 “샌드박스”가 아님
    quickjs-emscripten이 저수준이고 마법 같은 기능을 피하는 이유는 제공하는 API가 안전하다고 자신 있게 말하기 위해서임. 마법 같은 직렬화나 쉬운 네트워크/파일시스템 접근 기능 요청을 대체로 거절하는데, 이런 코드는 보안 실수가 생기기 좋은 영역이기 때문임
    신뢰할 수 없는 코드를 실행할 때는 샌드박스 자체뿐 아니라 샌드박스에 노출하는 API를 위해 작성한 코드까지 신중히 감사해야 함
    다른 HN 사용자가 Fetch 쿠키를 물어본 댓글을 보고 잠재적 보안 이슈를 알아차렸음
    더 읽을거리: Figma 플러그인 샌드박스 보안 글 https://www.figma.com/blog/how-we-built-the-figma-plugin-sys..., https://www.figma.com/blog/an-update-on-plugin-security/, Quickjs-emscripten README https://github.com/justjake/quickjs-emscripten

    • iframe을 쓰면, 이 라이브러리 사용 여부와 관계없이 fetch 쿠키 문제를 막을 수 있는지 궁금함. 아니면 iframe에서도 여전히 문제가 되는지 알고 싶음
  • 서버 쪽과 브라우저 쪽 모두에서 JavaScript를 샌드박싱하는 방법은 많지만, DOM 접근을 “샌드박싱”할 방법이 있는지는 모르겠음
    예를 들어 신뢰할 수 없는 서드파티에게 미리 정한 위치의 DOM 요소에만 접근을 주는 식임. 내가 알기로 이를 허용하는 유일한 기술은 iframe인데, 아쉽게도 무겁고 느림
    플러그인을 호스팅할 수 있는 앱을 만들고 있는데, 플러그인에 DOM 접근을 주면 말 그대로 무엇이든 할 수 있게 되는 것 같음

    • Salesforce는 웹 컴포넌트와 패치된 ShadowRoot를 조합해서 이걸 처리함. 섀도 루트 참조를 가진 코드가 문서의 나머지 영역으로 걸어 나가지 못하게 하고, SES(Secure EcmaScript)와 관련된 안전한 평가 함수로 신뢰할 수 없는 스크립트가 접근할 수 있는 전역 객체를 제한함
      그 안전한 평가 함수가 꽤 대단함. 핵심은 아마 여기 같음: https://github.com/Agoric/realms-shim/blob/v1.1.0/src/evalua...
      플랫폼 차원에서 이 문제를 풀기 위한 격리된 웹 컴포넌트 아이디어도 있음: https://github.com/WICG/webcomponents/issues/1002
    • 가장 가까운 것은 Allen Wirfs-Brock의 jsmirrors 프로토타입인데, DOM에 대해서는 명세화까지 가지 않았고 애초에 그럴 의도도 별로 없었던 것으로 앎. JavaScript라는 프로그래밍 시스템을 위한 권한(capability) 만 다뤘음
      jsmirrors에서 영감을 얻어 “dommirrors” 같은 것을 직접 시도해볼 수는 있지만 큰 작업임. jsmirrors를 그대로 써서 원하는 것을 어느 정도 달성하는 우회 방법도 있지만 쓰기 편하진 않음
      다만 DOM 접근을 매개하거나 시뮬레이션해서 주는 것은 거의 확실히 진짜 원하는 것이 아님. 상대가 실제로 무엇을 하게 허용하고 싶은지 파악한 뒤, 그 동작을 가능하게 하는 권한만 주는 편이 맞음
      예를 들어 어딘가에 버튼을 추가하게 하려면 부모 요소라는 앵커 지점을 주고 document.createElement로 DOM 노드를 만들게 해야 한다고 생각할 수 있음. 하지만 실제로 원하는 건 그들에게 document.createElement 접근권을 주는 게 아니라 add-button 권한임. 그러니 addButton을 구현해 주면 됨
      더 보기: <https://news.ycombinator.com/item?id=30703531#30706060>
      CSP가 이 용도라고 말하는 사람 말은 듣지 않는 게 좋음. 그렇지 않음. 정확히 말해 CSP가 원래 목표로 하는 용도에서도 설계가 나쁘고 사용자에게 적대적인 쓰레기라, 지금처럼 구현되고 확장되지 말았어야 한다고 봄. 여기에 의존하는 건 위험함
    • 정말 안전하게 접근하려면 서드파티 코드에 다른 도메인의 iframe을 주고 sandbox 속성을 설정하는 방법뿐임
      부모 페이지에서 DOM 콘텐츠 크기를 측정해 iframe 크기를 일정 한도 안에서 조절할 수 있으니, 앱 UI에 더 자연스럽게 통합할 수는 있음
      사용자 간 노출 수준과 신뢰 수준에 따라 iframe 안에서의 사칭/피싱과 클릭재킹 시도를 조심해야 함. 이상적으로는 프레임이 웹 요청을 전혀 못 하게 잠가야 하고, 이는 이미지 로딩도 안 된다는 뜻임
      그렇게 하면 예컨대 사용자가 가짜 비밀번호 폼에 비밀번호를 입력하도록 속더라도 프레임 밖으로 데이터를 유출할 방법이 없음
      iframe이 요청할 수 있는 리소스 종류를 제한하는 주된 방법은 Content-Security-Policy이며, 이를 통해 서드파티 이미지, 스크립트 등을 모두 끌 수 있음
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLIFrameE...
      https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
      또 다른 sandbox 속성도 켜고, 웹캠 같은 프라이버시 민감 DOM API 접근은 꺼야 함
      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...
    • 가능한 방법은 서드파티가 렌더링하고 싶은 HTML을 문자열로 보내는 API를 추가하고, 여러 라이브러리 중 하나로 이를 파싱한 뒤 파싱된 데이터 기반으로 DOM을 다시 만드는 방식일 듯함
      이렇게 하면 허용할 HTML 요소와 속성을 화이트리스트로 제한할 수 있음. 네이티브 DOM 이벤트 수신까지 허용하려면 복잡해지지만 불가능하진 않음
      이벤트 이름 문자열과 이벤트를 받을 요소 id를 받는 API 같은 것이 필요하고, 실제 DOM에서 해당 이벤트를 감지한 뒤 사용 중인 JavaScript 샌드박스 안으로 같은 이벤트를 복제해야 함. 그 샌드박스 안에서는 앞서 말한 API에 접근할 수 있어야 함
    • 즉석에서 생각하면 이런 식으로 해볼 수 있음: 백엔드에서 서드파티 코드를 요청해 그 코드와 해시 시퀀스를 연결함
      백엔드에서 HTML을 동적으로 수정해 id 값이 해시 시퀀스인 div 태그를 넣음. 또 HTML을 수정해 자기 도메인에서 서드파티 코드를 요청하는 script 태그를 넣고, 추적용으로 그 script 태그의 data 속성에 해시 값을 추가함
      백엔드에서 서드파티 코드를 수정해 document.window.의 모든 인스턴스를 document.getElementById(hash_value).로 바꾸고, 모든 쿼리 선택자가 #hash_value로 시작하게 함
      Element 프로토타입의 .parentNode를 커스텀 속성으로 대체해, 제공된 컨테이너에서 벗어나는 시도를 검사하고 차단해야 함
      그런 다음 HTML 문서를 브라우저로 보냄. 서드파티 코드가 깨져도 괜찮음. 제약은 서드파티에 알려야 하고, 서버로 보내기 전에 자기 코드 테스트는 그들이 해야 함
      중요한 건 코드가 동적으로 제공된 컨테이너를 벗어나지 못하게 하는 것뿐임. 보안 위반을 찾기 위해 이 부분은 정기적으로 테스트해야 함
      실제로 동작하지 않을 수도 있지만 실험해보면 재미있을 듯함
  • 우연히 지난주 QuickJS를 시험해 봤고, 결국 isolated-vm으로 정착함. 둘 다 보안 계약은 만족했지만, isolated-vm이 설정, 해체, eval 실행 오버헤드 면에서 훨씬 더 성능이 좋았음

  • 흥미로운 접근임. 워커로 격리하고 JavaScript 환경 정리 기법을 쓰는 다른 JavaScript 샌드박스 라이브러리[1] 작성자로서, JavaScript를 해석하는 방식, 즉 JavaScript 안의 JavaScript나 이 경우처럼 WASM 안의 JavaScript가 가장 높은 수준의 격리를 제공한다고 봄
    또한 호스트 JavaScript 가상 머신 자체의 버그에 직접 노출되지 않음. Node를 대상으로 한다면 더 중요할 수 있는데, 일부 최신 흐름을 제외하면 Node.js는 Deno와 달리 격리와 샌드박싱을 염두에 두고 설계된 것 같지 않기 때문임
    API를 보면 createRuntimefetch 외에 호스트 환경 호출을 정의할 수 있는지는 보이지 않음. 이 기능은 꽤 유용할 텐데, 외부 세계와의 통신을 전부 허용하거나 전부 막는 식이 아니라 통제된 방식으로 제한할 수 있기 때문임
    마찬가지로 브라우저도 지원하지 않는 것처럼 보임. 적어도 esm.sh로 간단히 확인했을 때는 그랬음. 이것도 유용한 기능일 수 있음
    오버헤드가 어느 정도인지 궁금해서 테스트해볼 예정이고, 앞서 말했듯 접근 자체는 꽤 탄탄해 보임
    [1] @exact-realty/lot

  • CPU가 너무 빨라졌으니 이제 인터프리터 안에서 인터프리터를 돌리자는 얘기처럼 보임

  • QuickJS에서 JavaScript를 실행하는 장점으로 성능을 들지는 않겠음. QuickJS는 호스트 JavaScript 가상 머신과는 전혀 경쟁이 안 됨
    다만 오래된 C 인터프리터나 JavaScript로 구현한 인터프리터보다는 빠를 수 있음

    • 스크립트 실행 시간보다 Node.js 프로세스 시작 시간이 지배적이라면 성능 이득이 있을 수 있음. 아마 상당수의 서버리스 함수형 스크립트가 여기에 해당할 것 같음
  • 이걸로 사용자가 제공한 JavaScript 코드를 실행할 수 있겠음. 사용자의 TypeScript 코드를 샌드박스 환경에서 번들러로 묶는 방법을 찾고 있었음
    QuickJS 안에서 webpack 같은 번들러를 실행하는 방법에 대한 추천이 있는지 궁금함

    • QJS로 하는 방법은 모르겠지만, 브라우저에서 번들러를 실행하고 싶다면 WebContainers가 바로 그런 용도로 만들어진 것 같음
      [1]: https://webcontainers.io/
  • 매우 멋짐. WASM으로 컴파일됐다면 브라우저에서 실행할 수 있는지 궁금함. 가능하면서도 요청에 쿠키를 붙이지 않고 fetch 요청을 할 수 있다면 흥미로울 듯함

  • 이전 직장에서 Quickjs-emscripten에서 segmentation fault와 조용한 오류가 너무 많이 나서 프로젝트가 보류됐음
    다시 해야 한다면 더 많은 프로그램에서 제대로 동작하고 공식적으로 승인된 WASM 번들이 있는 엔진을 쓸 것 같음

  • iframe으로 코드를 안전하게 샌드박싱할 수 있다고 생각했지만 확신은 없음. 물론 자체 인터프리터를 쓰면 더 촘촘한 시간 제한, 커스텀 API 같은 기능을 더 많이 넣을 수 있을 듯함

    • 나도 특히 서비스 워커까지 섞이면 이 부분을 더 알고 싶음
      지금은 window message로 코드를 받는 iframe을 쓰고 있고, 입력 코드를 평가한 뒤 window message로 응답을 돌려줄 수 있어서 꽤 잘 동작함. 다만 샌드박스 밖으로 빠져나갈 구멍이 있을지는 확신이 없음
      더 복잡한 경우, 예컨대 패키지 의존성은 Babel로 파싱한 다음 CDN(esm.sh)을 쓰는 import map을 만들려고 했지만, CDN화된 버전이 잘 동작하지 않는 경우가 있었음
      그래서 StackBlitz를 썼고 꽤 잘 동작하지만, 보안 컨텍스트가 아닌 환경에서는 문제가 좀 있음
      결국 코드와 의존성(package.json)을 받아 Docker 컨테이너 안에서 Vite 빌드를 수행하고 결과를 돌려주는 작은 웹 서버를 만들었음. 나름 괜찮게 동작하지만 가끔 느림
      완전히 클라이언트에서 빌드할 수 있으면 좋겠고, StackBlitz가 대략 그런 일을 하고 있음
    • iframe은 권한이 과하게 부여되어 있음. 예를 들어 iframe은 데이터를 서드파티로 유출할 수 있음