⚡ Electron5. 보안 모델01 — Threat Model

01 — Threat Model

한 줄 답: 같은 innerHTML += userInput 한 줄이, 브라우저에서는 origin 안의 데이터 누설에 그치지만, Electron에서는 — Node API가 renderer에 노출돼 있으면 — require('child_process').exec('rm -rf ~')OS RCE가 된다. 그래서 Electron의 위협 모델은 XSS = RCE라는 축약 등식에서 출발한다.


웹에서는 ~인데 Electron에서는 ~

웹에서는 XSS가 document.cookie 탈취·세션 hijack·DOM 위조까지가 끝이다 — 브라우저 sandbox + same-origin policy가 그 origin의 데이터 밖으로 나가는 것을 막는다. Electron에서는 XSS 한 줄이 Node 런타임 전체에 닿을 수 있다. nodeIntegration: true였던 v4 이전 디폴트에서는 require('child_process')로 바로 OS 명령이 실행됐고, 그 이후에도 preload가 무엇을 노출했느냐에 따라 간접 RCE 경로가 만들어진다.


Why — 왜 위협 모델부터 시작하나

보안 설정은 위협을 알고 깔아야 의미가 있다. 디폴트만 그대로 두면 안전할 것 같지만, Electron은 역사적으로 디폴트가 위험하던 시절이 길었다 — nodeIntegration: true, contextIsolation: false, remote 모듈 활성. 새 앱은 디폴트가 안전해졌지만, 기존 앱을 그대로 옮긴 코드 베이스가 많고, 학습 자료들도 옛 패턴을 답습한다.

흔한 오해실제
”renderer는 브라우저처럼 격리돼 있겠지”renderer는 Chromium 프로세스지만 어떤 권한이 켜져 있느냐webPreferences가 결정한다 — 디폴트만 믿으면 안 된다
”preload는 안전한 다리니까 거기 다 노출하면 됨”preload가 window.fs = require('fs')처럼 Node 객체 그대로를 노출하면, renderer가 그걸 통해 prototype을 오염시킬 수 있다
”XSS만 막으면 됨”XSS 외에도 외부 navigation, new-window, prototype pollution, supply chain(npm 패키지)이 모두 RCE 경로다

여기서 핵심은 공격의 시작 위치가 여러 군데라는 점이다. XSS만 단일 표적이 아니다.


How — XSS → RCE 경로의 5가지 정형

경로 도식

5가지 입구(E1E5)와 4가지 증폭기(N1N4)가 교차한다. 입구 하나, 증폭기 하나만 열려 있어도 경로가 완성된다.

경로 1: 직접 XSS + nodeIntegration

가장 고전적이고 가장 자주 보고된 패턴. Electron v4 이전까지 디폴트가 nodeIntegration: true였기 때문에, 그 시절에 빌드된 앱은 모두 이 경로에 노출돼 있었다.

{/* 어떤 채팅 메시지 렌더링 */}
<div id="msg"></div>
<script>
  // 공격자가 보낸 메시지
  const userMsg = "<img src=x onerror=\"require('child_process').exec('open /Applications/Calculator.app')\">";
  document.getElementById('msg').innerHTML = userMsg;
</script>

Discord 2018 (CVE-2018-15685) 가 정확히 이 패턴이었다 — Discord 채팅의 임베드 처리가 외부 iframe을 신뢰했고, 그 iframe이 nodeIntegration 켜진 채로 떴다. (상세는 07)

경로 2: 외부 navigation

renderer가 공격자가 통제하는 origin으로 이동하면, 그 origin의 JS가 현재 renderer의 권한을 그대로 상속한다. preload는 그 origin에도 attach된다.

// 어떤 채팅 링크 클릭 → 검증 없이 location 변경
document.getElementById('chat-link').onclick = (e) => {
  window.location.href = e.target.href; // attacker.com으로 이동
};

방어는 will-navigate 이벤트로 허용 origin만 통과시키는 것. (06)

경로 3: new-window

target=_blank 또는 window.open새 BrowserWindow를 만든다 — 디폴트로 부모와 같은 webPreferences. 즉 nodeIntegration이 켜져 있으면 새 창에도 켜진다.

<a href="file:///tmp/malicious.html" target="_blank">click</a>

file:// 로컬 HTML 파일에 <script>require('fs').writeFileSync(...)</script>가 들어 있으면 그대로 실행. 방어는 setWindowOpenHandler모든 new-window를 거절 또는 외부 브라우저 위임. (06)

경로 4: Prototype Pollution

contextIsolation: false였던 시절, renderer JS와 preload JS는 같은 V8 컨텍스트를 공유했다. 즉 renderer가 Object.prototype을 오염시키면 preload의 객체도 그 오염을 본다. preload가 if (opts.allowExec) ... 같은 옵션 객체를 쓰면, 공격자가 Object.prototype.allowExec = true를 심어 모든 옵션 객체에 그 키를 주입할 수 있다.

contextIsolation: true(v12+ 디폴트)가 두 컨텍스트를 V8 isolate로 완전 분리한다. (02)

경로 5: Supply Chain

가장 막기 어려운 경로. npm 의존성 트리에 postinstall 스크립트가 들어가면 — 빌드 머신에서 임의 코드 실행. event-stream(2018), ua-parser-js(2021), node-ipc(2022)가 모두 이 패턴.

→ Electron 자체로 막을 수 없고, lockfile · audit · 빌드 격리 · SBOM이 답.


What — 위협 모델 매트릭스

웹 vs Electron 위협 비교 (전수)

위협 유형브라우저 (웹)Electron차이의 근거
XSS (반사·저장·DOM)session hijack, cookie 탈취XSS → RCE 직결 가능Node API가 같은 프로세스에 있음
CSRF다른 origin의 요청 위조거의 무관 (cookie 기반 인증 드묾)Electron은 자체 인증 토큰 쓰는 경우가 일반
CORS bypass막혀 있음webSecurity: false꺼버리는 사고 흔함webSecurityoff-able
ClickjackingX-Frame-Options · CSP frame-ancestors동일하지만 외부 iframe이 nodeIntegration 상속 시 추가 위험iframe도 같은 webPreferences
Prototype pollutionorigin 안의 사고preload 오염 → Node API 흭득contextIsolation이 핵심 방어
Open redirect그냥 다른 origin으로 갈 뿐renderer가 공격자 origin에 머무름will-navigate로 막아야
File:// 접근막혀 있거나 매우 제한적디폴트로 훨씬 자유webSecurity + allowRunningInsecureContent
권한 prompt (mic·cam)브라우저가 UI로 띄움디폴트로 모두 허용 — 핸들러 등록 안 하면 통과setPermissionRequestHandler 미등록 시 허용 디폴트
Service Worker abuse브라우저가 격리거의 동일renderer가 브라우저 기능 상속
Native module RCE해당 없음네이티브 모듈 자체에 메모리 버그 가능C/C++ 코드가 같은 프로세스에
Auto-update MITM해당 없음업데이트 서버 위조 → 임의 binary 설치06에서 다룸
Supply chain (npm)빌드 단계에서 멈추면 끝빌드 → 사용자 PC까지 도달binary 배포의 본질

공격자 능력 모델 (3단계)

능력공격자가 할 수 있는 것막을 수 있는 가장 약한 옵션
L1 — 외부 URL 통제사용자가 클릭하는 링크의 도메인 통제will-navigate + CSP
L2 — 사용자 입력 통제채팅 메시지·코멘트·파일 이름에 임의 문자열 주입DOMPurify + CSP + contextIsolation
L3 — 임의 JS 실행 (XSS 성공)renderer에 JS 한 줄 심음nodeIntegration: false + sandbox: true + IPC 채널 검증

→ Electron 보안의 목표는 L3까지 가도 RCE가 안 되는 상태. 디폴트 옵션만 켜면 L1·L2까지는 안전하지만 L3에 진입하는 순간이 진실의 순간이다.


What-if — 위협 모델을 잘못 그리면

1) “디폴트가 안전하니까 그대로 두자”

→ 옛 자료·옛 boilerplate에는 디폴트 변경 전의 옵션이 명시적으로 적혀 있는 경우가 많다. 디폴트가 바뀌어도 명시된 값이 우선이다. 대응: webPreferences반드시 명시하고 07의 13항목 체크리스트로 대조.

2) “XSS만 막으면 안전”

→ navigation·new-window·permission·supply chain은 XSS와 독립된 경로. 대응: 5가지 경로 모두에 한 줄씩 대응 코드를 깐다.

3) “사용자 입력은 우리 서버를 거치니까 안전”

→ Electron 앱은 외부 임베드가 많다. 채팅에 외부 URL, 메시지에 외부 이미지, link preview, OAuth 리다이렉트, mailto: 등 공격자가 일부를 통제할 수 있는 입구가 많다. 대응: 모든 외부 origin을 iframe sandbox로 격리 + CSP로 차단.

4) “preload가 다리니까 거기에 fs를 노출하면 편하다”

→ preload가 window.fs = require('fs')처럼 객체 그대로 노출하면, renderer가 window.fs.constructor.constructor("...")()임의 함수를 만들 수 있다. 대응: contextBridge.exposeInMainWorld함수만, 인자도 1개씩 화이트리스트.

5) “binary로 배포하니까 코드는 안 보임”

→ Electron 앱은 asar 안에 원본 JS가 그대로 들어 있다. npx asar extract app.asar out/이면 끝. 대응: 민감 로직은 서버에. 코드 자체는 공개돼 있다고 가정하고 위협 모델을 그린다.


Insight — 흥미로운 이야기

”Discord 2018 — <iframe> 하나가 RCE가 된 날”

2018년 보안 연구자 Masato Kinugawa가 Discord 데스크톱에서 외부 임베드(youtube 등)의 iframe이 nodeIntegration이 켜진 채 로드되는 것을 발견했다. youtube embed의 hostname을 유사 도메인으로 위조하고, 그 페이지에 require('child_process').exec(...)를 넣어 PoC를 발표했다. CVE-2018-15685. Discord는 그 직후 모든 임베드를 sandbox iframe으로 강제 변경했고, 이 사건이 Electron 팀이 v5에서 contextIsolation 도입을 가속한 직접 트리거가 됐다.

→ 교훈: 외부 origin이 같은 webPreferences를 상속하는 것이 가장 위험한 디폴트였다.

”BadElectron — 2017년 LiveOverflow의 시연”

YouTuber LiveOverflow가 2017년 “modern Electron apps are vulnerable to XSS = RCE”라는 영상을 올리며, VS Code·Slack·Discord·Atom의 PoC를 차례로 보여줬다 (책임 있는 공개 후). 이 영상이 Electron 보안의 대중 인식을 만들었고, electronegativity 같은 정적 분석 도구가 출시되는 계기가 됐다.

→ 교훈: 위협 모델은 학술적이 아니라 실무다. 같은 패턴이 여러 유명 앱에 동시에 존재했다는 사실이 디폴트의 책임을 키웠다.

”Electron 팀의 위협 모델 문서”

Electron 공식 문서는 Security · Native Capabilities, and Your Responsibility라는 별도 페이지를 두고, *“Electron applications run with the same permissions as the operating system user”*라는 한 문장으로 위협 모델을 요약한다. 이 한 문장이 모든 13항목 체크리스트의 출발점이다.

→ 교훈: *위협 모델은 “사용자 권한 = 앱 권한”*이라는 한 줄로 환원된다. 이 한 줄을 받아들이지 않으면 모든 보안 옵션이 과해 보인다.


요약 + 다이어그램

Electron의 위협 모델은 XSS = RCE 가능이라는 축약 등식에서 출발한다. 입구는 5가지(XSS · navigation · new-window · prototype pollution · supply chain), 증폭기는 4가지(nodeIntegration · preload 누설 · IPC 검증 누락 · shell.openExternal). 다음 6개 문서가 각 증폭기를 차단하는 옵션을 하나씩 다룬다.

다음 문서: 02-context-isolation.md — 첫 번째 격리층, renderer JS와 preload JS를 다른 V8 컨텍스트로 분리하는 메커니즘.