Electron Fuses
Electron 앱은 본질적으로 Node.js 런타임을 내장한 브라우저다. 이 말은 Electron 바이너리 안에 Node.js의 모든 기능이 들어있다는 뜻이고, 이 중 상당수는 프로덕션 앱에서 전혀 쓰이지 않으면서 공격 표면만 넓히는 역할을 한다.
예를 들어 ELECTRON_RUN_AS_NODE라는 환경 변수가 있다. 이걸 설정하면 Electron 앱이 일반 Node.js처럼 동작한다. 앱 아이콘을 클릭했는데 Node.js REPL이 뜨는 셈이다. 개발 중에는 유용할 수 있지만 프로덕션에서는 공격자가 이 변수를 설정해서 앱의 코드 서명을 우회하고 임의의 Node.js 코드를 실행할 수 있다. 이런 공격을 "living off the land"라고 부른다. 시스템에 이미 설치된 정상적인 바이너리를 악용하는 것이다.
그러면 이런 기능을 비활성화하려면 어떻게 해야 할까? 가장 확실한 방법은 Electron을 직접 빌드하면서 해당 코드를 제거하는 것이다. 하지만 Electron을 소스에서 빌드하는 건 시간도 오래 걸리고 빌드 인프라도 필요하며, 업스트림 변경사항을 계속 따라가야 하는 유지보수 부담이 크다. 개인 프로젝트든 회사 프로젝트든 현실적이지 않다.
Fuses는 이 문제를 해결하기 위해 만들어졌다.
Fuses의 동작 원리
Fuses는 Electron 바이너리 안에 박혀 있는 "매직 비트"다. 바이너리 파일 안에 특정 위치에 특정 바이트 패턴이 있고, 이 비트를 0 또는 1로 설정해서 기능을 켜거나 끌 수 있다.
핵심은 이 비트가 패키징 시점에 변경된다는 것이다. 앱을 빌드하고 패키징할 때 바이너리를 직접 수정한 뒤 코드 서명을 한다. 그 이후에는 OS의 코드 서명 검증 메커니즘(macOS의 Gatekeeper, Windows의 AppLocker 등)이 바이너리가 변조되지 않았는지 보장한다.
이 순서가 중요하다:
- Electron 바이너리에서 fuse 비트를 변경 (패키징 시)
- 변경된 바이너리를 코드 서명
- OS가 코드 서명을 검증 → 이후 fuse 비트를 되돌릴 수 없음
런타임에 환경 변수나 설정 파일로 제어하는 것과 근본적으로 다르다. 런타임 설정은 공격자가 바꿀 수 있지만, 코드 서명된 바이너리의 비트는 바꿀 수 없다. 바꾸면 서명이 깨지고 OS가 실행을 거부한다.
Fuse 목록
Electron이 제공하는 fuse들을 하나씩 살펴보자. 각 fuse가 어떤 기능을 제어하는지, 왜 비활성화하는 게 좋은지 이해하는 것이 중요하다.
RunAsNode
- 기본값: 활성화 (enabled)
- 권장: 비활성화
ELECTRON_RUN_AS_NODE 환경 변수의 동작 여부를 제어한다. 이 변수가 설정되면 Electron 앱이 일반 Node.js 런타임처럼 동작한다. GUI 창이 뜨지 않고 stdin/stdout으로 동작하는 Node.js 프로세스가 된다.
비활성화하면 공격자가 이 환경 변수로 앱을 악용할 수 없다. 단, child_process.fork()가 내부적으로 이 환경 변수에 의존하기 때문에 더 이상 동작하지 않는다. 자식 프로세스가 필요하다면 Electron의 Utility Process API를 사용해야 한다.
[FuseV1Options.RunAsNode]: false
EnableCookieEncryption
- 기본값: 비활성화 (disabled)
- 권장: 활성화
쿠키 저장소의 암호화 여부를 제어한다. Chromium은 내부적으로 SQLite 데이터베이스에 쿠키를 저장하는데, 기본적으로 값이 평문(plaintext)으로 저장된다. 이 fuse를 활성화하면 OS 레벨의 암호화 키를 사용해서 쿠키를 암호화한다. Chrome 브라우저가 하는 것과 동일한 방식이다.
주의할 점이 하나 있다. 이 전환은 단방향이다. 활성화하면 기존 평문 쿠키가 쓰기 시 자동으로 암호화된다. 하지만 나중에 다시 비활성화하면 암호화된 쿠키를 읽을 수 없게 되어 쿠키 저장소가 손상된다. 한번 켜면 끄지 말아야 한다.
[FuseV1Options.EnableCookieEncryption]: true
EnableNodeOptionsEnvironmentVariable
- 기본값: 활성화 (enabled)
- 권장: 비활성화
NODE_OPTIONS와 NODE_EXTRA_CA_CERTS 환경 변수의 동작 여부를 제어한다. NODE_OPTIONS는 Node.js 런타임에 다양한 옵션을 전달할 수 있는 변수다. --max-old-space-size=4096처럼 메모리 제한을 바꾸거나, --require로 임의의 모듈을 로드하는 것도 가능하다.
프로덕션 앱에서 이 변수를 사용할 일이 거의 없다. 반면 공격자에게는 런타임 동작을 조작할 수 있는 강력한 도구가 된다. 비활성화가 안전하다.
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false
EnableNodeCliInspectArguments
- 기본값: 활성화 (enabled)
- 권장: 비활성화
--inspect, --inspect-brk 같은 CLI 디버깅 플래그의 동작 여부를 제어한다. 이 플래그들은 Chrome DevTools에서 Node.js 프로세스에 원격으로 연결할 수 있게 해주는 디버깅 기능이다. 개발 중에는 유용하지만, 프로덕션에서 활성화되면 공격자가 디버거를 연결해서 앱의 메모리를 읽거나 코드를 주입할 수 있다.
비활성화하면 SIGUSR1 시그널로 메인 프로세스의 인스펙터를 시작하는 것도 차단된다.
[FuseV1Options.EnableNodeCliInspectArguments]: false
EnableEmbeddedAsarIntegrityValidation
- 기본값: 비활성화 (disabled)
- 권장: 활성화
ASAR 아카이브의 무결성 검증 기능이다. Electron 앱의 소스 코드는 일반적으로 app.asar라는 아카이브 파일로 패키징된다. 이 fuse를 활성화하면 macOS와 Windows에서 app.asar를 로드할 때 내용이 변조되지 않았는지 검증한다.
약간의 성능 오버헤드가 있다. app.asar 안의 파일을 읽을 때마다 해시를 검증하기 때문이다. 하지만 실제로 체감할 정도는 아니고, 보안상 이점이 훨씬 크다.
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true
OnlyLoadAppFromAsar
- 기본값: 비활성화 (disabled)
- 권장: 활성화
Electron이 앱 코드를 찾는 순서를 제어한다. 기본적으로 Electron은 다음 순서로 앱 코드를 탐색한다:
app.asar(ASAR 아카이브)app(일반 폴더)default_app.asar(Electron 기본 앱)
이 fuse를 활성화하면 app.asar에서만 코드를 로드한다. 공격자가 app 폴더에 악성 코드를 넣어도 무시된다. EnableEmbeddedAsarIntegrityValidation과 함께 사용하면 "검증된 ASAR에서만 코드 로드"라는 강력한 보안 체인이 만들어진다.
[FuseV1Options.OnlyLoadAppFromAsar]: true
GrantFileProtocolExtraPrivileges
- 기본값: 활성화 (enabled)
- 권장: 비활성화 (커스텀 프로토콜 사용 시)
file:// 프로토콜로 로드된 페이지에 추가 권한을 부여하는지 제어한다. 활성화 상태에서 file:// 페이지는 다음이 가능하다:
fetch로 다른file://리소스 로드- Service Worker 사용
- 자식 프레임에 대한 범용 접근 (샌드박스 설정 무시)
이건 Electron 초기 버전의 레거시 동작이다. 현재 Electron 공식 가이드는 file:// 대신 커스텀 프로토콜(app:// 등)을 사용하도록 권장한다. 커스텀 프로토콜을 쓰고 있다면 이 fuse를 비활성화해도 된다.
LoadBrowserProcessSpecificV8Snapshot
- 기본값: 비활성화 (disabled)
- 상황에 따라 활성화
V8 스냅샷 파일을 프로세스별로 분리한다. 기본적으로 Electron의 모든 프로세스(메인, 렌더러)가 같은 V8 스냅샷을 사용한다. 이 fuse를 활성화하면 메인 프로세스는 browser_v8_context_snapshot.bin이라는 별도 스냅샷을 사용한다.
보안 관점에서, 렌더러 프로세스가 nodeIntegration이 활성화된 스냅샷을 사용하지 않도록 분리할 수 있다. 대부분의 앱에서는 굳이 활성화할 필요가 없지만, 보안 요구사항이 높은 앱에서는 고려할 만하다.
Fuses 적용 방법
@electron/fuses CLI
@electron/fuses 패키지의 flipFuses 함수를 사용해서 빌드 스크립트에서 직접 fuse를 변경할 수 있다.
import { flipFuses, FuseVersion, FuseV1Options } from '@electron/fuses';
await flipFuses(
require('electron'), // Electron 바이너리 경로
{
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}
);
flipFuses가 하는 일은 단순하다. Electron 바이너리 파일을 열고, 특정 오프셋에 있는 fuse 바이트를 찾아서 값을 변경한다. 파일 I/O 레벨의 작업이다.
Electron Forge 플러그인
Electron Forge를 사용한다면 @electron-forge/plugin-fuses 플러그인으로 더 깔끔하게 설정할 수 있다. forge.config.ts의 plugins 배열에 추가하면 패키징 과정에서 자동으로 fuse가 적용된다.
import { FusesPlugin } from '@electron-forge/plugin-fuses';
import { FuseV1Options, FuseVersion } from '@electron/fuses';
const config: ForgeConfig = {
// ...
plugins: [
// 다른 플러그인들...
new FusesPlugin({
version: FuseVersion.V1,
[FuseV1Options.RunAsNode]: false,
[FuseV1Options.EnableCookieEncryption]: true,
[FuseV1Options.EnableNodeOptionsEnvironmentVariable]: false,
[FuseV1Options.EnableNodeCliInspectArguments]: false,
[FuseV1Options.EnableEmbeddedAsarIntegrityValidation]: true,
[FuseV1Options.OnlyLoadAppFromAsar]: true,
}),
],
};
이 방식의 장점은 fuse 설정이 빌드 설정의 일부로 관리된다는 것이다. 별도의 빌드 스크립트를 작성할 필요 없이 설정 파일만 수정하면 된다.
현재 fuse 상태 확인
빌드된 앱의 fuse 상태를 확인하고 싶다면 @electron/fuses 패키지의 CLI를 사용한다.
npx @electron/fuses read --app /path/to/your/app
이 명령은 바이너리를 읽어서 각 fuse의 현재 상태(활성화/비활성화)를 출력한다. CI/CD 파이프라인에서 빌드 후 fuse가 제대로 적용됐는지 검증하는 용도로 쓸 수 있다.
보안 관점에서의 Fuses
Fuses가 효과적인 이유는 방어의 깊이(Defense in Depth) 원칙을 실현하기 때문이다.
일반적인 보안 설정은 런타임에 적용된다. 환경 변수, 설정 파일, 커맨드 라인 인자 등. 이것들은 모두 공격자가 실행 환경을 제어할 수 있다면 우회 가능하다.
Fuses는 바이너리 레벨에서 기능을 제거한다. 그리고 코드 서명이 그 변경을 봉인한다. 공격자가 fuse를 되돌리려면 바이너리를 수정해야 하고, 바이너리를 수정하면 코드 서명이 깨지고, 코드 서명이 깨지면 OS가 실행을 거부한다.
이게 "living off the land" 공격을 막는 핵심이다. 공격자가 시스템에 이미 설치된 Electron 앱을 악용해서 임의 코드를 실행하는 공격 벡터를 원천적으로 차단한다.
프로덕션 앱이라면 최소한 다음 fuse는 반드시 설정해야 한다:
RunAsNode: false— 환경 변수로 Node.js 모드 진입 차단EnableNodeOptionsEnvironmentVariable: false— NODE_OPTIONS 주입 차단EnableNodeCliInspectArguments: false— 원격 디버거 연결 차단EnableCookieEncryption: true— 쿠키 평문 저장 방지OnlyLoadAppFromAsar: true+EnableEmbeddedAsarIntegrityValidation: true— 검증된 코드만 실행
이 여섯 가지만 설정해도 Electron 앱의 보안 포스처가 크게 개선된다.
정리
- Fuses는 바이너리 레벨에서 기능을 제거하는 방식이라 런타임 설정과 달리 공격자가 우회할 수 없다
- 패키징 시점에 비트를 변경하고 코드 서명으로 봉인하는 순서가 보안의 핵심이다
- RunAsNode, NodeOptions, InspectArguments 비활성화 + CookieEncryption, AsarIntegrity, OnlyLoadFromAsar 활성화가 프로덕션 최소 권장 세트다