Skip to main content

Breach to Barrier: Strengthening Apps with the Sandbox

· 8 min read

CVE-2023-4863: WebP의 힙 버퍼 오버플로우가 공개된 지 일주일이 넘었고, 이로 인해 webp 이미지를 렌더링하는 소프트웨어의 새로운 버전이 속속 출시되었다. macOS, iOS, Chrome, Firefox, 그리고 다양한 리눅스 배포판 모두 업데이트를 받았다. 이는 Citizen Lab의 조사에 따른 것으로, "워싱턴 DC에 기반을 둔 시민 사회 조직"이 사용하는 iPhone이 iMessage 내부의 제로 클릭 익스플로잇을 통해 공격받고 있음을 발견한 결과였다.

Electron 또한 이에 대응해 같은 날 새로운 버전을 출시했다. 만약 여러분의 앱이 사용자가 제공한 콘텐츠를 렌더링한다면, Electron 버전을 업데이트해야 한다. v27.0.0-beta.2, v26.2.1, v25.8.1, v24.8.3, 그리고 v22.3.24 버전 모두 webp 이미지 렌더링을 담당하는 libwebp 라이브러리의 수정된 버전을 포함하고 있다.

이제 "이미지 렌더링"과 같이 무해해 보이는 상호작용도 잠재적으로 위험한 활동일 수 있다는 사실을 모두가 새롭게 인지하게 되었다. 이 기회를 통해 Electron이 프로세스 샌드박스를 제공하여 다음 대규모 공격의 피해 범위를 제한할 수 있다는 점을 상기시키고자 한다. 이 공격이 무엇이든 간에 말이다.

샌드박스는 Electron v1부터 사용 가능했고, v20부터는 기본적으로 활성화되었다. 하지만 많은 앱(특히 오랫동안 사용되어 온 앱)이 코드 어딘가에 sandbox: false를 설정했거나, nodeIntegration: true를 설정하여 명시적인 sandbox 설정이 없을 때 샌드박스를 비활성화했을 가능성이 있다. 이는 이해할 수 있다. 오랫동안 Electron을 사용해왔다면, HTML/CSS를 실행하는 동일한 코드에 require("child_process")require("fs")를 추가하는 강력한 기능을 즐겼을 것이다.

샌드박스로 마이그레이션하는 방법에 대해 이야기하기 전에, 먼저 샌드박스를 사용해야 하는지 논의해보자.

샌드박스는 모든 렌더러 프로세스 주위에 견고한 케이지를 둔다. 이는 내부에서 무슨 일이 일어나든 코드가 제한된 환경 내에서 실행되도록 보장한다. 이 개념은 Chromium보다 훨씬 오래되었으며, 모든 주요 운영체제에서 기능으로 제공된다. Electron과 Chromium의 샌드박스는 이러한 시스템 기능 위에 구축된다. 사용자 생성 콘텐츠를 표시하지 않더라도, 렌더러가 손상될 가능성을 고려해야 한다. 공급망 공격과 같은 정교한 시나리오부터 작은 버그까지, 렌더러가 의도하지 않은 동작을 할 수 있다.

샌드박스는 이러한 시나리오를 훨씬 덜 무섭게 만든다. 내부 프로세스는 CPU 사이클과 메모리를 자유롭게 사용할 수 있지만, 그 이상의 권한은 없다. 프로세스는 디스크에 쓸 수도 없고, 자신의 윈도우를 표시할 수도 없다. libwebp 버그의 경우, 샌드박스는 공격자가 악성코드를 설치하거나 실행할 수 없도록 보장한다. 사실, 직원의 iPhone을 대상으로 한 원래의 Pegasus 공격에서, 공격자는 일반적으로 샌드박스된 iMessage의 경계를 벗어나기 위해 비 샌드박스된 이미지 프로세스를 특정적으로 타겟팅했다. 이 예제와 같은 CVE가 발표되면, 여전히 Electron 앱을 안전한 버전으로 업그레이드해야 하지만, 그동안 공격자가 할 수 있는 피해의 양은 크게 제한된다.

sandbox: false에서 sandbox: true로 바닐라 Electron 애플리케이션을 마이그레이션하는 것은 상당한 작업이다. 나는 Electron 보안 가이드라인의 초안을 직접 작성했음에도 불구하고, 내 앱 중 하나를 마이그레이션하지 못했음을 알고 있다. 이번 주말에 그 작업을 마쳤고, 여러분도 그렇게 하기를 권장한다.

라인 변경 수에 겁먹지 마세요. 대부분은 package-lock.json에 있습니다.

두 가지 작업을 해야 한다:

  1. preload 스크립트나 실제 WebContents에서 Node.js 코드를 사용하고 있다면, 모든 Node.js 상호작용을 메인 프로세스(또는, 고급스럽게 유틸리티 프로세스)로 옮겨야 한다. 렌더러가 얼마나 강력해졌는지 고려하면, 대부분의 코드는 리팩토링이 필요하지 않을 가능성이 높다.

    프로세스 간 통신에 대한 문서를 참고하라. 내 경우에는 많은 코드를 옮기고 ipcRenderer.invoke()ipcMain.handle()로 감쌌지만, 과정은 직관적이었고 빠르게 완료되었다. 여기서 API에 대해 조금 신경 써야 한다. 만약 executeCodeAsRoot(code)와 같은 API를 만든다면, 샌드박스가 사용자를 크게 보호해주지 못할 것이다.

  2. 샌드박스를 활성화하면 preload 스크립트에서 Node.js 통합이 비활성화되므로, 더 이상 require("../my-script")를 사용할 수 없다. 즉, preload 스크립트는 단일 파일이어야 한다.

    이를 위한 여러 방법이 있다: Webpack, esbuild, parcel, 그리고 rollup 모두 이 작업을 처리할 수 있다. 나는 Electron Forge의 훌륭한 Webpack 플러그인을 사용했고, electron-builder의 사용자는 electron-webpack을 사용할 수 있다.

전체적으로 이 과정은 약 4일이 걸렸다. Webpack의 강력한 기능을 어떻게 다룰지 고민하는 시간도 포함되었는데, 이 기회를 통해 코드를 다른 방식으로도 리팩토링하기로 결정했기 때문이다.