[Electron] 보안 강화

HTTPS만 사용하기

HTTP나 WS는 중간자 공격에 취약하므로, 모든 외부 리소스는 HTTPS를 사용해야 합니다.

// main.js
// Bad: HTTP
mainWindow.loadURL('http://example.com');

// Good: HTTPS
mainWindow.loadURL('https://example.com');

<!-- index.html -->
<!-- Bad -->
<script src="http://cdn.example.com/lib.js"></script>

<!-- Good -->
<script src="https://cdn.example.com/lib.js"></script>

원격 콘텐츠에는 Node.js 비활성화

원격 콘텐츠를 로드할 때 nodeIntegration를 꺼두고, 필요한 기능은 preload.js 스크립트로 제한적으로 제공합니다.

// main.js
const win = new BrowserWindow({
  webPreferences: {
    nodeIntegration: false,    // Node.js 통합 비활성화
    contextIsolation: true,    // 컨텍스트 격리 활성화
    preload: path.join(__dirname, 'preload.js')
  }
});
win.loadURL('https://example.com');

// preload.js
const { contextBridge, ipcRenderer } = require('electron');

contextBridge.exposeInMainWorld('api', {
  // 필요한 기능만 안전하게 노출
  fetchData: () => ipcRenderer.invoke('fetch-data')
});

컨텍스트 격리 필수 적용

contextIsolation을 활성화해 렌더러와 Electron 환경을 분리, 악성 스크립트의 API 접근을 차단합니다.

// main.js
const win = new BrowserWindow({
  webPreferences: {
    contextIsolation: true,
    preload: path.join(__dirname, 'preload.js')
  }
});

프로세스 샌드박스 활성화

렌더러 프로세스가 시스템 리소스에 직접 접근하지 못하도록 샌드박스를 적용합니다.

// main.js
app.enableSandbox(); // 앱 전체에 샌드박스 강제 활성화

const win = new BrowserWindow({
  webPreferences: {
    sandbox: true   // 개별 창에서도 명시 가능
  }
});

권한 요청 제어

원격 콘텐츠가 권한(알림, 위치 등)을 요청할 때, 반드시 검증 로직을 거쳐야 합니다.

// main.js
const { session } = require('electron');

session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
  const url = new URL(webContents.getURL());
  // example.com만 허용
  if (permission === 'notifications' && url.origin === 'https://example.com') {
    return callback(true);
  }
  callback(false);
});

웹 보안 유지

webSecurity: false는 Same-Origin Policy를 해제하여 보안 구멍을 만듭니다.

// main.js
// Bad
new BrowserWindow({ webPreferences: { webSecurity: false } });

// Good
new BrowserWindow({});  // 기본값 유지

CSP로 콘텐츠 제한

CSP를 통해 허용된 스크립트·스타일 소스를 엄격히 제한합니다. XSS 공격을 방어하는 강력한 수단입니다.

<!-- index.html -->
<meta http-equiv="Content-Security-Policy"
      content="default-src 'self'; script-src 'self' https://apis.example.com;">
// 또는 main.js에서 http 헤더로 설정
// main.js
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
  callback({
    responseHeaders: {
      ...details.responseHeaders,
      'Content-Security-Policy': ["default-src 'self'"]
    }
  });
});

혼합 콘텐츠 차단

allowRunningInsecureContent를 사용하면 HTTPS 페이지에서 HTTP 리소스 로드를 허용하는 것은 혼합 콘텐츠 취약점을 만듭니다.

// main.js
// Bad
new BrowserWindow({ webPreferences: { allowRunningInsecureContent: true } });

// Good
new BrowserWindow({});

실험적 기능 배제

experimentalFeatures나 enableBlinkFeatures는 보안 위험이 있으므로 사용하지 않습니다.

// main.js
// Bad
new BrowserWindow({ webPreferences: { experimentalFeatures: true } });

// Good
new BrowserWindow({});

WebView 팝업 제한

<webview allowpopups>를 사용하면 외부 페이지가 새 창을 마음대로 생성할 수 있어 위험합니다.

<!-- index.html -->
<!-- Bad -->
<webview allowpopups src="page.html"></webview>

<!-- Good -->
<webview src="page.html"></webview>

WebView 옵션 검증

will-attach-webview 이벤트를 활용해 동적으로 생성되는 <webview>의 옵션을 검사·제어합니다.

// main.js
app.on('web-contents-created', (e, contents) => {
  contents.on('will-attach-webview', (event, webPrefs, params) => {
    // preload 스크립트 제거
    delete webPrefs.preload;
    // Node.js 통합 금지
    webPrefs.nodeIntegration = false;
    // 허용된 URL만 로드
    if (!params.src.startsWith('https://example.com/')) {
      event.preventDefault();
    }
  });
});

네비게이션 및 창 제어

will-navigate와 setWindowOpenHandler로 허용된 도메인·URL만 허용하고, 그 외 요청은 차단합니다.

// main.js
app.on('web-contents-created', (e, contents) => {
  contents.on('will-navigate', (event, url) => {
    if (new URL(url).origin !== 'https://example.com') event.preventDefault();
  });
  contents.setWindowOpenHandler(({ url }) => {
    // 외부 링크는 시스템 브라우저로만 열기
    if (url.startsWith('https://example.com')) {
      return { action: 'allow' };
    }
    shell.openExternal(url);
    return { action: 'deny' };
  });
});

최신 Electron 버전 사용

최신 버전은 Chromium과 Node.js의 보안 패치를 포함하므로 필수입니다.

IPC 메시지 송신자 검증

ipcMain.handle·ipcMain.on으로 수신한 메시지의 event.senderFrame.url을 검증하여, 신뢰된 출처만 허용합니다.

// main.js
ipcMain.handle('get-secret', (event) => {
  const origin = new URL(event.senderFrame.url).origin;
  if (origin !== 'https://example.com') {
    throw new Error('Unauthorized');
  }
  return getSecretData();
});

Electron API 노출 최소화

contextBridge.exposeInMainWorld로 필요한 메서드만 안전하게 래핑해 노출하고, 원본 ipcRenderer나 Electron API 객체 전체는 절대 노출하지 않습니다.

// preload.js
// Bad
contextBridge.exposeInMainWorld('api', { send: ipcRenderer.send });

// Good
contextBridge.exposeInMainWorld('api', {
  doAction: (arg) => ipcRenderer.invoke('do-action', arg)
});