보안
Electron 취약점을 올바르게 공개하는 방법에 대한 정보는 SECURITY.md를 참고한다.
Chromium 업스트림 취약점의 경우: Electron은 Chromium 릴리스를 교대로 최신 상태로 유지한다. 자세한 내용은 Electron 릴리스 타임라인 문서를 확인한다.
서문
웹 개발자로서 우리는 대체로 브라우저가 제공하는 강력한 보안망을 누리며 작업한다. 우리가 작성한 코드와 관련된 위험은 상대적으로 작다. 우리의 웹사이트는 샌드박스 내에서 제한된 권한을 부여받으며, 사용자들이 대규모 엔지니어 팀이 구축한 브라우저를 통해 새롭게 발견된 보안 위협에 신속히 대응할 수 있기를 기대한다.
Electron을 다룰 때는 Electron이 웹 브라우저가 아니라는 점을 이해하는 것이 중요하다. Electron은 익숙한 웹 기술을 사용해 기능이 풍부한 데스크톱 애플리케이션을 구축할 수 있게 해주지만, 우리의 코드는 훨씬 더 큰 권한을 갖는다. JavaScript는 파일 시스템과 사용자 셸 등에 접근할 수 있다. 이는 고품질의 네이티브 애플리케이션을 구축할 수 있게 해주지만, 코드에 부여된 추가 권한만큼 보안 위험도 커진다.
이를 염두에 두고, 신뢰할 수 없는 소스에서 임의의 콘텐츠를 표시하는 것은 Electron이 처리하도록 설계되지 않은 심각한 보안 위험을 초래할 수 있음을 인지해야 한다. 실제로 가장 인기 있는 Electron 애플리케이션(Atom, Slack, Visual Studio Code 등)은 주로 로컬 콘텐츠(또는 Node 통합 없이 신뢰할 수 있는 보안 원격 콘텐츠)를 표시한다. 만약 애플리케이션이 온라인 소스에서 코드를 실행한다면, 해당 코드가 악성 코드가 아니라는 것을 보장하는 것은 여러분의 책임이다.
일반 가이드라인
보안은 모두의 책임이다
Electron 애플리케이션의 보안은 프레임워크의 기반(Chromium, Node.js), Electron 자체, 모든 NPM 의존성, 그리고 여러분의 코드 전반의 보안 상태에 의해 결정된다는 점을 기억해야 한다. 따라서 다음과 같은 중요한 모범 사례를 따르는 것은 여러분의 책임이다:
-
애플리케이션을 최신 Electron 프레임워크 버전으로 유지한다. 제품을 출시할 때는 Electron, Chromium 공유 라이브러리, Node.js로 구성된 번들도 함께 배포한다. 이러한 컴포넌트에 영향을 미치는 취약점은 애플리케이션의 보안에 영향을 줄 수 있다. Electron을 최신 버전으로 업데이트하면 _nodeIntegration 우회_와 같은 주요 취약점이 이미 패치되어 애플리케이션에서 악용될 수 없도록 보장한다. 자세한 내용은 "최신 버전의 Electron 사용"을 참고한다.
-
의존성을 평가한다. NPM은 50만 개가 넘는 재사용 가능한 패키지를 제공하지만, 신뢰할 수 있는 서드파티 라이브러리를 선택하는 것은 여러분의 책임이다. 알려진 취약점이 있는 구형 라이브러리를 사용하거나 유지보수가 제대로 되지 않는 코드에 의존하면 애플리케이션의 보안이 위험에 처할 수 있다.
-
보안 코딩 관행을 도입한다. 애플리케이션의 첫 번째 방어선은 여러분의 코드이다. 크로스 사이트 스크립팅(XSS)과 같은 일반적인 웹 취약점은 Electron 애플리케이션에 더 큰 보안 영향을 미칠 수 있으므로, 보안 소프트웨어 개발 모범 사례를 도입하고 보안 테스트를 수행하는 것이 매우 권장된다.
신뢰할 수 없는 콘텐츠의 격리
신뢰할 수 없는 소스(예: 원격 서버)에서 코드를 받아 로컬에서 실행할 때는 항상 보안 문제가 발생할 수 있다. 예를 들어, 기본 BrowserWindow
내부에 원격 웹사이트를 표시하는 경우를 생각해 보자. 공격자가 해당 콘텐츠를 변경할 수 있다면(소스를 직접 공격하거나 앱과 실제 목적지 사이에 위치하는 방식으로), 사용자의 머신에서 네이티브 코드를 실행할 수 있게 된다.
절대 Node.js 통합을 활성화한 상태에서 원격 코드를 로드하고 실행하지 마라. 대신, Node.js 코드를 실행할 때는 앱과 함께 패키징된 로컬 파일만 사용해야 한다. 원격 콘텐츠를 표시하려면 <webview>
태그나 WebContentsView
를 사용하고, nodeIntegration
을 비활성화하고 contextIsolation
을 활성화해야 한다.
보안 경고와 권장 사항은 개발자 콘솔에 출력된다. 이 경고는 바이너리 이름이 Electron일 때만 표시되며, 개발자가 현재 콘솔을 보고 있다는 것을 나타낸다.
process.env
나 window
객체에 ELECTRON_ENABLE_SECURITY_WARNINGS
또는 ELECTRON_DISABLE_SECURITY_WARNINGS
를 설정하여 이러한 경고를 강제로 활성화하거나 비활성화할 수 있다.
보안 권장사항 체크리스트
애플리케이션 보안을 강화하려면 최소한 다음 단계를 따르는 것이 좋다:
- 안전한 콘텐츠만 로드하기
- 원격 콘텐츠를 표시하는 모든 렌더러에서 Node.js 통합 비활성화
- 모든 렌더러에서 컨텍스트 격리 활성화
- 프로세스 샌드박싱 활성화
- 원격 콘텐츠를 로드하는 모든 세션에서
ses.setPermissionRequestHandler()
사용 webSecurity
비활성화하지 않기Content-Security-Policy
정의 및 엄격한 규칙 사용 (예:script-src 'self'
)allowRunningInsecureContent
활성화하지 않기- 실험적 기능 활성화하지 않기
enableBlinkFeatures
사용하지 않기<webview>
:allowpopups
사용하지 않기<webview>
: 옵션과 파라미터 검증- 네비게이션 비활성화 또는 제한
- 새 윈도우 생성 비활성화 또는 제한
- 신뢰할 수 없는 콘텐츠에서
shell.openExternal
사용하지 않기 - Electron 최신 버전 사용
- 모든 IPC 메시지의
sender
검증 file://
프로토콜 사용 피하고 커스텀 프로토콜 사용 선호- 변경 가능한 퓨즈 확인
- 신뢰할 수 없는 웹 콘텐츠에 Electron API 노출하지 않기
설정 오류와 불안전한 패턴을 자동으로 감지하려면 Electronegativity를 사용할 수 있다. Electron을 사용해 애플리케이션을 개발할 때 발생할 수 있는 잠재적 취약점과 구현 버그에 대한 추가 정보는 개발자 및 감사자를 위한 가이드를 참고한다.
애플리케이션에 포함되지 않은 모든 리소스는 HTTPS
와 같은 보안 프로토콜을 사용해 로드해야 한다. 다시 말해, HTTP
와 같은 비보안 프로토콜은 사용하지 않아야 한다. 마찬가지로 WS
대신 WSS
, FTP
대신 FTPS
등을 사용하는 것을 권장한다.
왜 HTTPS를 사용해야 할까?
HTTPS
는 크게 두 가지 주요 이점을 제공한다:
- 데이터 무결성을 보장한다. 애플리케이션과 호스트 간에 전송되는 데이터가 중간에 변조되지 않았음을 확인할 수 있다.
- 사용자와 목적지 호스트 간의 트래픽을 암호화한다. 이로 인해 애플리케이션과 호스트 간에 전송되는 정보를 도청하기가 더 어려워진다.
이 두 가지 기능은 보안과 개인 정보 보호를 강화하는 데 중요한 역할을 한다. 특히 민감한 정보를 다루는 웹 애플리케이션에서는 HTTPS 사용이 필수적이다.
어떻게?
// 나쁜 예
browserWindow.loadURL('http://example.com')
// 좋은 예
browserWindow.loadURL('https://example.com')
<!-- 나쁜 예 -->
<script crossorigin src="http://example.com/react.js"></script>
<link rel="stylesheet" href="http://example.com/style.css">
<!-- 좋은 예 -->
<script crossorigin src="https://example.com/react.js"></script>
<link rel="stylesheet" href="https://example.com/style.css">
2. 원격 콘텐츠에 Node.js 통합을 활성화하지 마세요
이 권장사항은 Electron 5.0.0부터 기본 동작입니다.
원격 콘텐츠를 로드하는 모든 렌더러(BrowserWindow
, WebContentsView
, 또는 <webview>
)에서 Node.js 통합을 활성화하지 않는 것이 매우 중요합니다. 이는 원격 콘텐츠에 부여하는 권한을 제한하여, 공격자가 여러분의 웹사이트에서 JavaScript를 실행할 수 있는 능력을 얻더라도 사용자에게 피해를 입히기 훨씬 더 어렵게 만드는 것이 목표입니다.
이후에는 특정 호스트에 대해 추가 권한을 부여할 수 있습니다. 예를 들어, https://example.com/
을 가리키는 BrowserWindow를 열 경우, 해당 웹사이트에 필요한 정확한 능력만 부여하고 그 이상은 주지 않도록 설정할 수 있습니다.
이유는 무엇인가?
크로스 사이트 스크립팅(XSS) 공격은 공격자가 렌더러 프로세스를 벗어나 사용자의 컴퓨터에서 코드를 실행할 수 있다면 더 위험해진다. XSS 공격은 비교적 흔하게 발생하는데, 문제가 있긴 하지만 일반적으로 해당 웹사이트 내에서만 영향을 미친다. Node.js 통합을 비활성화하면 XSS가 원격 코드 실행(RCE) 공격으로 확장되는 것을 방지할 수 있다.
어떻게 할까?
// 나쁜 예
const mainWindow = new BrowserWindow({
webPreferences: {
contextIsolation: false,
nodeIntegration: true,
nodeIntegrationInWorker: true
}
})
mainWindow.loadURL('https://example.com')
// 좋은 예
const mainWindow = new BrowserWindow({
webPreferences: {
preload: path.join(app.getAppPath(), 'preload.js')
}
})
mainWindow.loadURL('https://example.com')
<!-- 나쁜 예 -->
<webview nodeIntegration src="page.html"></webview>
<!-- 좋은 예 -->
<webview src="page.html"></webview>
Node.js 통합을 비활성화하더라도 Node.js 모듈이나 기능을 사용하는 API를 웹사이트에 노출할 수 있다. 프리로드 스크립트는 require
및 기타 Node.js 기능에 계속 접근할 수 있으므로, 개발자는 contextBridge API를 통해 원격으로 로드된 콘텐츠에 커스텀 API를 노출할 수 있다.
3. 컨텍스트 격리 활성화
Electron 12.0.0부터 컨텍스트 격리는 기본 동작이다.
컨텍스트 격리는 Electron의 기능 중 하나로, 개발자가 프리로드 스크립트와 Electron API에서 코드를 실행할 때 전용 자바스크립트 컨텍스트를 사용할 수 있게 한다. 실제로 이는 Array.prototype.push
나 JSON.parse
와 같은 전역 객체가 렌더러 프로세스에서 실행되는 스크립트에 의해 수정될 수 없음을 의미한다.
Electron은 이 동작을 구현하기 위해 Chromium의 콘텐츠 스크립트와 동일한 기술을 사용한다.
nodeIntegration: false
를 사용하더라도, 강력한 격리를 보장하고 Node 기본 기능의 사용을 방지하려면 contextIsolation
도 반드시 사용해야 한다.
contextIsolation
이 무엇인지, 그리고 어떻게 활성화하는지에 대한 자세한 내용은 전용 문서 컨텍스트 격리를 참고한다.
4. 프로세스 샌드박싱 활성화
샌드박싱은 Chromium의 기능 중 하나로, 운영체제를 활용해 렌더러 프로세스의 접근 권한을 크게 제한한다. 모든 렌더러에서 샌드박스를 활성화해야 한다. 신뢰할 수 없는 컨텐츠를 메인 프로세스를 포함한 샌드박스가 적용되지 않은 프로세스에서 로드하거나 읽거나 처리하는 것은 권장하지 않는다.
프로세스 샌드박싱이 무엇인지, 그리고 어떻게 활성화하는지에 대한 자세한 내용은 전용 문서 프로세스 샌드박싱을 참고한다.
5. 원격 콘텐츠의 세션 권한 요청 처리
Chrome을 사용하다 보면 권한 요청을 본 적이 있을 것이다. 이는 웹사이트가 사용자의 수동 승인이 필요한 기능(예: 알림)을 사용하려고 할 때 나타난다.
이 API는 Chromium 권한 API를 기반으로 하며, 동일한 유형의 권한을 구현한다.
왜?
기본적으로 Electron은 개발자가 커스텀 핸들러를 수동으로 설정하지 않는 한 모든 권한 요청을 자동으로 승인한다. 이는 안정적인 기본값이지만, 보안을 중시하는 개발자라면 반대의 접근 방식을 고려할 수 있다.
어떻게?
const { session } = require('electron')
const { URL } = require('url')
session
.fromPartition('some-partition')
.setPermissionRequestHandler((webContents, permission, callback) => {
const parsedUrl = new URL(webContents.getURL())
if (permission === 'notifications') {
// 권한 요청을 승인한다
callback(true)
}
// URL을 확인한다
if (parsedUrl.protocol !== 'https:' || parsedUrl.host !== 'example.com') {
// 권한 요청을 거부한다
return callback(false)
}
})
6. webSecurity
를 비활성화하지 마라
이 권장 사항은 Electron의 기본 설정이다.
렌더러 프로세스(BrowserWindow
, WebContentsView
, 또는 <webview>
)에서 webSecurity
속성을 비활성화하면 중요한 보안 기능이 꺼진다는 것을 이미 추측했을 것이다.
프로덕션 애플리케이션에서는 webSecurity
를 절대 비활성화하지 마라.
왜 이 설정을 사용할까?
webSecurity
를 비활성화하면 동일 출처 정책(same-origin policy)이 해제되고, allowRunningInsecureContent
속성이 true
로 설정된다. 이는 다른 도메인에서 불안전한 코드를 실행할 수 있도록 허용한다는 의미이다.
어떻게?
// 나쁜 예
const mainWindow = new BrowserWindow({
webPreferences: {
webSecurity: false
}
})
// 좋은 예
const mainWindow = new BrowserWindow()
<!-- 나쁜 예 -->
<webview disablewebsecurity src="page.html"></webview>
<!-- 좋은 예 -->
<webview src="page.html"></webview>
7. 콘텐츠 보안 정책 정의하기
콘텐츠 보안 정책(Content Security Policy, CSP)은 크로스 사이트 스크립팅 공격과 데이터 주입 공격을 방어하기 위한 추가 보안 계층이다. Electron 내부에서 로드하는 모든 웹사이트에 CSP를 활성화할 것을 권장한다.
왜 필요한가?
CSP(Content Security Policy)는 서버가 제공하는 콘텐츠에 대해 로드할 수 있는 리소스를 제한하고 제어할 수 있게 해준다. 예를 들어, https://example.com
에서 정의한 오리진의 스크립트는 로드할 수 있도록 허용하지만, https://evil.attacker.com
과 같은 악의적인 공격자의 스크립트는 실행되지 않도록 차단할 수 있다. CSP를 정의하는 것은 애플리케이션의 보안을 강화하는 간단하면서도 효과적인 방법이다.
어떻게?
다음 CSP 설정을 사용하면 Electron이 현재 웹사이트와 apis.example.com
에서 스크립트를 실행할 수 있다.
// 나쁜 예시
Content-Security-Policy: '*'
// 좋은 예시
Content-Security-Policy: script-src 'self' https://apis.example.com
CSP HTTP 헤더
Electron은 Content-Security-Policy
HTTP 헤더를 지원한다. 이 헤더는 Electron의 webRequest.onHeadersReceived
핸들러를 사용해 설정할 수 있다:
const { session } = require('electron')
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': ['default-src \'none\'']
}
})
})
CSP 메타 태그
CSP(Content Security Policy)를 적용하는 기본 방식은 HTTP 헤더를 사용하는 것이다. 하지만 file://
프로토콜을 통해 리소스를 로드할 때는 이 방법을 사용할 수 없다. 이런 경우, HTML 마크업에 직접 <meta>
태그를 사용해 정책을 설정하면 유용하다.
<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
8. allowRunningInsecureContent
를 활성화하지 마라
이 권장 사항은 Electron의 기본 설정이다.
기본적으로 Electron은 HTTPS
를 통해 로드된 웹사이트가 불안전한 소스(HTTP
)에서 스크립트, CSS, 플러그인을 로드하고 실행하는 것을 허용하지 않는다. allowRunningInsecureContent
속성을 true
로 설정하면 이러한 보호 기능이 비활성화된다.
웹사이트의 초기 HTML을 HTTPS
로 로드한 후, 이후 리소스를 HTTP
를 통해 로드하려는 시도를 "혼합 콘텐츠(mixed content)"라고 부른다.
왜 HTTPS를 사용해야 할까?
HTTPS
를 통해 콘텐츠를 로드하면 리소스의 진위성과 무결성을 보장할 수 있으며, 동시에 트래픽 자체를 암호화한다. 자세한 내용은 안전한 콘텐츠만 표시하기 섹션을 참고한다.
어떻게?
// 나쁜 예
const mainWindow = new BrowserWindow({
webPreferences: {
allowRunningInsecureContent: true
}
})
// 좋은 예
const mainWindow = new BrowserWindow({})
9. 실험적 기능을 활성화하지 말 것
이 권장사항은 Electron의 기본 설정이다.
Electron의 고급 사용자는 experimentalFeatures
속성을 사용해 Chromium의 실험적 기능을 활성화할 수 있다.
왜 실험적 기능을 조심해야 할까?
실험적 기능은 이름 그대로 실험 단계에 있다. 모든 Chromium 사용자에게 활성화된 상태가 아니며, Electron 전체에 미치는 영향도 충분히 검증되지 않았다.
이 기능을 사용해야 할 타당한 이유가 있지만, 정확히 무엇을 하는지 모른다면 이 속성을 활성화하지 않는 것이 좋다.
어떻게?
// 나쁜 예
const mainWindow = new BrowserWindow({
webPreferences: {
experimentalFeatures: true
}
})
// 좋은 예
const mainWindow = new BrowserWindow({})
10. enableBlinkFeatures
사용 금지
이 권장사항은 Electron의 기본 설정이다.
Blink는 Chromium의 렌더링 엔진 이름이다. experimentalFeatures
와 마찬가지로, enableBlinkFeatures
속성을 사용하면 기본적으로 비활성화된 기능을 개발자가 활성화할 수 있다.
왜 기본값이 아닌가?
일반적으로 특정 기능이 기본값으로 활성화되지 않은 데는 타당한 이유가 있다. 특정 기능을 활성화해야 하는 합리적인 사용 사례가 존재한다. 개발자는 해당 기능을 왜 활성화해야 하는지, 어떤 영향을 미치는지, 애플리케이션의 보안에 어떤 변화를 가져오는지 정확히 이해해야 한다. 단순히 추측만으로 기능을 활성화해서는 안 된다.
어떻게?
// 나쁜 예
const mainWindow = new BrowserWindow({
webPreferences: {
enableBlinkFeatures: 'ExecCommandInJavaScript'
}
})
// 좋은 예
const mainWindow = new BrowserWindow()
11. WebView에서 allowpopups
사용을 피하라
이 권장사항은 Electron의 기본 설정이다.
<webview>
를 사용할 때, <webview>
태그 안에 로드된 페이지와 스크립트가 새 창을 열어야 할 필요가 있을 수 있다. allowpopups
속성을 사용하면 window.open()
메서드를 통해 새로운 BrowserWindows
를 생성할 수 있다. 그렇지 않으면 <webview>
태그는 새 창을 생성할 수 없다.
왜 필요한가?
팝업이 필요하지 않다면, 기본적으로 새로운 BrowserWindows
생성을 허용하지 않는 것이 좋다. 이는 최소 권한 원칙을 따르는 것이다. 웹사이트가 해당 기능이 필요하다는 것을 명확히 알기 전까지는 새로운 팝업을 생성할 수 없도록 제한해야 한다.
어떻게 할까?
<!-- 나쁜 예 -->
<webview allowpopups src="page.html"></webview>
<!-- 좋은 예 -->
<webview src="page.html"></webview>
12. WebView 생성 전 옵션 검증하기
Node.js 통합이 비활성화된 렌더러 프로세스에서 생성된 WebView는 통합을 자체적으로 활성화할 수 없다. 하지만 WebView는 항상 독립적인 렌더러 프로세스를 생성하며, 이 프로세스는 자신만의 webPreferences
를 갖는다.
새로운 <webview>
태그 생성을 메인 프로세스에서 제어하고, webPreferences
가 보안 기능을 비활성화하지 않았는지 확인하는 것이 좋다.
왜 중요한가?
<webview>
는 DOM 내에 존재하기 때문에, Node.js 통합이 비활성화된 상태에서도 웹사이트에서 실행 중인 스크립트에 의해 생성될 수 있다.
Electron은 개발자가 렌더러 프로세스를 제어하는 다양한 보안 기능을 비활성화할 수 있도록 한다. 대부분의 경우, 개발자는 이러한 기능을 비활성화할 필요가 없다. 따라서 새로 생성된 <webview>
태그에 대해 다른 설정을 허용해서는 안 된다.
어떻게 할까?
<webview>
태그가 부착되기 전에, Electron은 호스팅 중인 webContents
에서 will-attach-webview
이벤트를 발생시킨다. 이 이벤트를 활용해 보안상 위험할 수 있는 옵션을 가진 webView
의 생성을 방지할 수 있다.
app.on('web-contents-created', (event, contents) => {
contents.on('will-attach-webview', (event, webPreferences, params) => {
// 사용되지 않는 preload 스크립트를 제거하거나, 그 위치가 적법한지 확인한다.
delete webPreferences.preload
// Node.js 통합을 비활성화한다.
webPreferences.nodeIntegration = false
// 로드되는 URL을 검증한다.
if (!params.src.startsWith('https://example.com/')) {
event.preventDefault()
}
})
})
이러한 조치는 위험을 최소화할 뿐, 완전히 제거하지는 못한다. 만약 웹사이트를 표시하는 것이 목적이라면, 브라우저를 사용하는 것이 더 안전한 선택일 수 있다.
13. 네비게이션 비활성화 또는 제한
앱이 네비게이션을 전혀 필요로 하지 않거나 특정 페이지로만 이동해야 하는 경우, 해당 범위 내에서만 네비게이션을 허용하고 다른 모든 종류의 네비게이션을 차단하는 것이 좋다.
왜 중요한가?
네비게이션은 흔히 사용되는 공격 벡터다. 공격자가 앱이 현재 페이지에서 벗어나도록 유도하면, 인터넷 상의 웹사이트를 강제로 열게 만들 수 있다. webContents
가 더 안전하게 설정되어 있어도(예: nodeIntegration
비활성화 또는 contextIsolation
활성화), 앱이 임의의 웹사이트를 열도록 만들면 앱을 공격하기가 훨씬 쉬워진다.
일반적인 공격 패턴은 공격자가 앱 사용자를 속여 특정 방식으로 상호작용하도록 유도하는 것이다. 이로 인해 앱이 공격자의 페이지로 이동하게 된다. 이는 주로 링크, 플러그인, 또는 사용자가 생성한 콘텐츠를 통해 이루어진다.
어떻게?
앱에서 네비게이션이 필요하지 않다면, will-navigate
핸들러에서 event.preventDefault()
를 호출한다. 앱이 이동할 수 있는 페이지를 알고 있다면, 이벤트 핸들러에서 URL을 확인하고 예상하는 URL과 일치할 때만 네비게이션을 허용한다.
URL을 비교할 때는 Node의 파서를 사용하는 것을 권장한다. 단순한 문자열 비교는 때때로 속임수에 걸릴 수 있다. 예를 들어, startsWith('https://example.com')
테스트는 https://example.com.attacker.com
을 통과시킬 수 있다.
const { URL } = require('url')
const { app } = require('electron')
app.on('web-contents-created', (event, contents) => {
contents.on('will-navigate', (event, navigationUrl) => {
const parsedUrl = new URL(navigationUrl)
if (parsedUrl.origin !== 'https://example.com') {
event.preventDefault()
}
})
})
앱에서 새로운 윈도우 생성을 제한하거나 비활성화하는 것이 좋다. 이미 알려진 윈도우 집합이 있다면, 추가 윈도우 생성을 제한하는 것이 바람직하다.
왜 필요할까?
네비게이션과 마찬가지로, 새로운 webContents
를 생성하는 것은 흔한 공격 경로 중 하나다. 공격자는 여러분의 앱이 더 많은 권한을 가진 새로운 윈도우, 프레임, 혹은 다른 렌더러 프로세스를 생성하도록 유도하려 한다. 또는 이전에는 열 수 없었던 페이지를 열도록 시도한다.
앱이 필요로 하는 윈도우 외에 추가적인 윈도우를 생성할 필요가 없다면, 생성 기능을 비활성화하는 것이 추가적인 보안을 무료로 얻는 방법이 될 수 있다. 이는 일반적으로 하나의 BrowserWindow
를 열고 런타임 중에 임의의 수의 추가 윈도우를 열 필요가 없는 앱에 해당한다.
어떻게 구현할까?
webContents
는 새로운 윈도우를 생성하기 전에 window open handler에 위임한다. 이 핸들러는 윈도우가 열리도록 요청된 url
과 생성에 사용된 옵션을 포함한 여러 매개변수를 받는다. 예상치 못한 윈도우 생성 요청을 차단하기 위해 핸들러를 등록해 윈도우 생성 과정을 모니터링하는 것을 권장한다.
const { app, shell } = require('electron')
app.on('web-contents-created', (event, contents) => {
contents.setWindowOpenHandler(({ url }) => {
// 이 예제에서는 운영체제가 이벤트의 URL을 기본 브라우저에서 열도록 요청한다.
//
// shell.openExternal을 통해 허용할 URL에 대한 고려 사항은 다음 항목을 참고한다.
if (isSafeForExternalOpen(url)) {
setImmediate(() => {
shell.openExternal(url)
})
}
return { action: 'deny' }
})
})
15. 신뢰할 수 없는 콘텐츠에 shell.openExternal
을 사용하지 마라
shell
모듈의 openExternal
API는 데스크톱의 기본 유틸리티를 사용해 특정 프로토콜 URI를 열 수 있게 해준다. 예를 들어 macOS에서는 이 함수가 터미널의 open
명령어 유틸리티와 유사하게 동작하며, URI와 파일 타입 연결에 따라 특정 애플리케이션을 실행한다.
왜 주의해야 할까?
openExternal
을 부적절하게 사용하면 사용자의 호스트를 위험에 빠뜨릴 수 있다. 신뢰할 수 없는 콘텐츠와 함께 openExternal
을 사용하면 임의의 커맨드를 실행하는 데 악용될 가능성이 있다. 따라서 이 기능을 사용할 때는 항상 신뢰할 수 있는 출처의 콘텐츠만 처리해야 한다.
어떻게?
// 나쁜 예
const { shell } = require('electron')
shell.openExternal(USER_CONTROLLED_DATA_HERE)
// 좋은 예
const { shell } = require('electron')
shell.openExternal('https://example.com/index.html')
16. 최신 버전의 Electron 사용하기
항상 사용 가능한 최신 버전의 Electron을 사용하려고 노력해야 한다. 새로운 주요 버전이 출시될 때마다 가능한 한 빠르게 앱을 업데이트해야 한다.
왜 중요한가?
Electron, Chromium, Node.js의 오래된 버전을 사용해 만든 애플리케이션은 최신 버전을 사용한 애플리케이션보다 보안 위협에 노출되기 쉽다. 일반적으로 Chromium과 Node.js의 구버전에서 발생하는 보안 문제와 취약점은 더 널리 알려져 있으며, 이를 악용하는 방법도 더 많이 공개되어 있다.
Chromium과 Node.js는 수천 명의 뛰어난 개발자들이 만든 엔지니어링의 걸작이다. 이들의 인기 덕분에, 보안 역시 능력 있는 보안 연구자들에 의해 꼼꼼히 테스트되고 분석된다. 많은 연구자들은 책임 있는 취약점 공개 방식을 따르며, 이는 일반적으로 Chromium과 Node.js가 문제를 수정할 시간을 충분히 확보한 후에 취약점을 공개한다는 것을 의미한다. 따라서 잠재적인 보안 문제가 널리 알려지지 않은 최신 버전의 Electron(그리고 Chromium, Node.js)을 사용하면 애플리케이션의 보안이 더 강화된다.
방법
Electron의 주요 버전을 하나씩 순차적으로 마이그레이션한다. 이 과정에서 주요 변경 사항 문서를 참고해 코드 업데이트가 필요한 부분을 확인한다.
모든 IPC 메시지의 sender
를 검증해야 한다. 신뢰할 수 없는 렌더러에서 오는 메시지에 대해 작업을 수행하거나 정보를 전송하지 않도록 보장하기 위해서다.
왜 중요한가?
모든 웹 프레임은 이론적으로 메인 프로세스에 IPC 메시지를 보낼 수 있다. 여기에는 특정 상황에서의 iframe과 자식 윈도우도 포함된다. 만약 event.reply
를 통해 사용자 데이터를 보내거나, 렌더러가 기본적으로 수행할 수 없는 권한이 필요한 작업을 하는 IPC 메시지가 있다면, 반드시 제3자 웹 프레임에서 오는 메시지를 수신하지 않도록 해야 한다.
기본적으로 모든 IPC 메시지의 sender
를 검증하는 것이 중요하다.
어떻게?
// 나쁜 예
ipcMain.handle('get-secrets', () => {
return getSecrets()
})
// 좋은 예
ipcMain.handle('get-secrets', (e) => {
if (!validateSender(e.senderFrame)) return null
return getSecrets()
})
function validateSender (frame) {
// URL 파서와 허용 목록을 사용해 URL의 호스트를 검증
if ((new URL(frame.url)).host === 'electronjs.org') return true
return false
}
18. file://
프로토콜 사용을 피하고 커스텀 프로토콜 사용을 권장한다
로컬 페이지를 제공할 때 file://
프로토콜 대신 커스텀 프로토콜을 사용하는 것이 좋다.
왜 필요한가?
file://
프로토콜은 웹 브라우저보다 Electron에서 더 많은 권한을 가지며, 브라우저에서도 http/https URL과 다르게 처리된다. 커스텀 프로토콜을 사용하면 클래식 웹 URL 동작에 더 가깝게 맞출 수 있고, 무엇을 언제 로드할지에 대해 더 많은 제어권을 가질 수 있다.
file://
에서 실행되는 페이지는 사용자 컴퓨터의 모든 파일에 무제한으로 접근할 수 있다. 이는 XSS 문제가 발생할 경우 사용자의 컴퓨터에서 임의의 파일을 로드하는 데 악용될 수 있음을 의미한다. 커스텀 프로토콜을 사용하면 특정 파일 세트만 제공하도록 제한할 수 있어 이러한 문제를 방지할 수 있다.
방법
커스텀 프로토콜을 통해 파일이나 콘텐츠를 제공하는 방법을 배우려면 protocol.handle
예제를 참고한다.
19. 변경 가능한 퓨즈 확인하기
Electron은 다양한 옵션을 제공하지만, 대부분의 애플리케이션은 이를 필요로 하지 않는다. 직접 Electron 버전을 빌드하지 않고도 이러한 옵션을 끄거나 켤 수 있는 방법이 있다. 바로 퓨즈를 사용하는 것이다.
왜 필요한가?
runAsNode
와 nodeCliInspect
같은 퓨즈는 특정 환경 변수나 커맨드라인 인자를 사용해 애플리케이션을 실행할 때 다른 동작을 하도록 만든다. 이를 통해 애플리케이션을 이용해 디바이스에서 커맨드를 실행할 수 있다.
이 기능은 외부 스크립트가 직접 실행할 수 없는 커맨드를 애플리케이션의 권한으로 실행할 수 있게 해준다.
어떻게 할까?
이러한 퓨즈를 쉽게 전환할 수 있도록 @electron/fuses
모듈을 만들었다. 사용 방법과 발생할 수 있는 오류 사례에 대한 자세한 내용은 해당 모듈의 README를 참고하고, 문서의 How do I flip the fuses? 부분을 확인해 보자.
20. 신뢰할 수 없는 웹 콘텐츠에 Electron API 노출 금지
프리로드 스크립트에서 신뢰할 수 없는 웹 콘텐츠에 Electron의 API, 특히 IPC를 직접 노출해서는 안 된다.
왜 필요한가?
ipcRenderer.on
과 같은 원시 API를 직접 노출하는 것은 위험하다. 렌더러 프로세스가 전체 IPC 이벤트 시스템에 직접 접근할 수 있게 되어, 의도된 이벤트뿐만 아니라 모든 IPC 이벤트를 수신할 수 있기 때문이다.
이러한 노출을 방지하기 위해 콜백을 직접 전달하는 것도 피해야 한다. IPC 이벤트 콜백의 첫 번째 인자는 IpcRendererEvent
객체로, sender
와 같은 속성을 포함하며, 이는 기본 ipcRenderer
인스턴스에 접근할 수 있게 한다. 특정 이벤트만 수신하더라도 콜백을 직접 전달하면 렌더러가 이 이벤트 객체에 접근할 수 있다.
요약하면, 신뢰할 수 없는 웹 콘텐츠가 필요한 정보와 API에만 접근할 수 있도록 제한하는 것이 중요하다.
어떻게?
// 나쁜 예
contextBridge.exposeInMainWorld('electronAPI', {
on: ipcRenderer.on
})
// 역시 나쁜 예
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', callback)
})
// 좋은 예
contextBridge.exposeInMainWorld('electronAPI', {
onUpdateCounter: (callback) => ipcRenderer.on('update-counter', (_event, value) => callback(value))
})
contextIsolation
이 무엇인지, 그리고 이를 활용해 앱을 어떻게 보안할 수 있는지 더 자세히 알고 싶다면 Context Isolation 문서를 참고한다.