Electron 보안 체크리스트 — contextBridge 제대로 쓰기
nodeIntegration: true로 보안 감사에 걸렸다
처음 Electron 앱을 만들 때, 나는 편의를 위해 `nodeIntegration: true`로 설정했다. 렌더러 프로세스에서 Node.js API를 직접 쓸 수 있어서 편했다. 하지만 1년 뒤 보안 감사에서 걸렸다. "이건 심각한 보안 문제"라고.
그 뒤로 Electron 보안을 제대로 배웠다.
기본 보안 설정
<?xml version="1.0"?>
// main.ts
import { app, BrowserWindow } from 'electron'
function createWindow() {
const win = new BrowserWindow({
webPreferences: {
nodeIntegration: false, // 반드시 false
contextIsolation: true, // 반드시 true
enableRemoteModule: false, // 반드시 false
sandbox: true, // 반드시 true
preload: path.join(__dirname, 'preload.ts')
}
})
win.loadFile('index.html')
}
app.on('ready', createWindow)
Preload 스크립트로 안전하게 API 노출
<?xml version="1.0"?>
// preload.ts
import { contextBridge, ipcRenderer } from 'electron'
// 렌더러 프로세스에서 사용할 수 있는 API만 노출
contextBridge.exposeInMainWorld('electronAPI', {
// IPC 통신 래퍼
readFile: (filePath: string) =>
ipcRenderer.invoke('file:read', filePath),
writeFile: (filePath: string, content: string) =>
ipcRenderer.invoke('file:write', { filePath, content }),
getAppVersion: () =>
ipcRenderer.invoke('app:get-version'),
// 윈도우 제어
closeWindow: () =>
ipcRenderer.send('window:close'),
// 절대 이렇게 하면 안 됨!
// require: (module: string) => require(module) // 위험!
})
// 타입 정의
declare global {
interface Window {
electronAPI: {
readFile: (path: string) => Promise<string>
writeFile: (path: string, content: string) => Promise<void>
getAppVersion: () => Promise<string>
closeWindow: () => void
}
}
}
메인 프로세스에서 IPC 핸들러
<?xml version="1.0"?>
// main.ts
import { app, BrowserWindow, ipcMain } from 'electron'
import * as fs from 'fs/promises'
import * as path from 'path'
// 파일 읽기 (권한 확인)
ipcMain.handle('file:read', async (event, filePath: string) => {
// 보안: 특정 디렉토리만 접근 허용
const appDataPath = app.getPath('appData')
const resolvedPath = path.resolve(filePath)
if (!resolvedPath.startsWith(appDataPath)) {
throw new Error('Access denied')
}
return await fs.readFile(resolvedPath, 'utf-8')
})
// 파일 쓰기 (검증)
ipcMain.handle('file:write', async (event, { filePath, content }) => {
const appDataPath = app.getPath('appData')
const resolvedPath = path.resolve(filePath)
if (!resolvedPath.startsWith(appDataPath)) {
throw new Error('Access denied')
}
// 파일 크기 제한 (DDoS 방지)
if (content.length > 10 * 1024 * 1024) { // 10MB
throw new Error('File too large')
}
await fs.writeFile(resolvedPath, content, 'utf-8')
})
ipcMain.handle('app:get-version', async () => {
return app.getVersion()
})
ipcMain.on('window:close', (event) => {
BrowserWindow.fromWebContents(event.sender)?.close()
})
렌더러 프로세스에서 사용
<?xml version="1.0"?>
// renderer.ts (또는 React 컴포넌트)
async function loadUserData() {
try {
const data = await window.electronAPI.readFile('user-data.json')
const config = JSON.parse(data)
console.log(config)
} catch (error) {
console.error('Failed to load data:', error)
}
}
async function saveUserData(config: any) {
try {
await window.electronAPI.writeFile(
'user-data.json',
JSON.stringify(config, null, 2)
)
} catch (error) {
console.error('Failed to save data:', error)
}
}
// React에서 사용
function Settings() {
const [config, setConfig] = useState(null)
useEffect(() => {
loadUserData().then(setConfig)
}, [])
return (
<div>
{config && <p>Version: {window.electronAPI.getAppVersion()}</p>}
</div>
)
}
CSP (Content Security Policy)**
<?xml version="1.0"?>
// index.html에 CSP 설정
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<!-- 중요: 강력한 CSP 설정 -->
<meta
http-equiv="Content-Security-Policy"
content="
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data:;
font-src 'self';
"
/>
<title>My App</title>
</head>
<body>
<div id="root"></div>
<script src="index.js"></script>
</body>
</html>
또는 코드에서:
<?xml version="1.0"?>
// main.ts
function createWindow() {
const win = new BrowserWindow({...})
win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
responseHeaders: {
...details.responseHeaders,
'Content-Security-Policy': [
"default-src 'self'; " +
"script-src 'self'; " +
"style-src 'self' 'unsafe-inline';"
]
}
})
})
}
업데이트 보안
<?xml version="1.0"?>
// 자동 업데이트는 HTTPS만 사용
import { autoUpdater } from 'electron-updater'
autoUpdater.checkForUpdatesAndNotify()
// 서명 확인
autoUpdater.on('before-download', () => {
// 먼저 다운로드 URL 검증
const url = autoUpdater.currentVersion.releaseUrl
if (!url.startsWith('https://')) {
throw new Error('Updates must use HTTPS')
}
})
보안 체크리스트
- ☐ nodeIntegration: false
- ☐ contextIsolation: true
- ☐ enableRemoteModule: false
- ☐ sandbox: true
- ☐ preload 스크립트 사용
- ☐ IPC 핸들러에서 경로 검증
- ☐ 파일 크기 제한
- ☐ CSP 헤더 설정
- ☐ 민감한 정보 암호화
- ☐ 자동 업데이트는 HTTPS만
- ☐ 의존성 보안 감사
마무리
Electron의 보안은 기본이다. 편의를 위해 nodeIntegration을 켜는 건 절대 금지다. contextBridge와 IPC는 조금 번거롭지만, 그만큼의 보안을 제공한다.
처음부터 제대로 하면, 나중에 보안 감사에서 헤맬 일이 없다.