security2025-05-10·8·74/348

Express.js helmet 보안 헤더 설정 완벽 가이드

Express.js에서 helmet 미들웨어를 사용하여 보안 HTTP 헤더를 설정하는 방법을 다룹니다.

Introduction

웹 애플리케이션의 보안을 강화하기 위해 HTTP 보안 헤더를 설정하는 것은 필수적입니다. Express.js에서 helmet 미들웨어는 다양한 보안 헤더를 쉽게 설정할 수 있게 해줍니다.

Environment

# 프로젝트 의존성
npm list express helmet
# express@4.18.2
# helmet@7.1.0

Problem

현재 앱에 보안 헤더가 설정되어 있지 않아 various 보안 문제가 발생했습니다:

# 보안 검사 결과
$ security-headers-cli check https://myapp.com

- Content-Security-Policy: Not set
- X-Content-Type-Options: Not set
- X-Frame-Options: Not set
- Strict-Transport-Security: Not set
- X-XSS-Protection: Not set

Grade: F

Analysis

보안 헤더 중요성

헤더목적필수도
CSPXSS 공격 방지높음
HSTSHTTPS 강제높음
X-Content-Type-OptionsMIME 스니핑 방지중간
X-Frame-Options클릭재킹 방지중간
Referrer-Policy정보 유출 방지중간

Solution

1. helmet 기본 설정

const express = require('express');
const helmet = require('helmet');

const app = express();

// helmet 기본 설정 적용
app.use(helmet());

// 또는 개별 설정
app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'"],
        styleSrc: ["'self'", "'unsafe-inline'"],
        imgSrc: ["'self'", "data:", "https:"],
        fontSrc: ["'self'", "https://fonts.gstatic.com"],
        connectSrc: ["'self'", "https://api.myapp.com"],
        frameSrc: ["'none'"],
        objectSrc: ["'none'"],
    },
}));

2. 상세 보안 헤더 설정

const express = require('express');
const helmet = require('helmet');

const app = express();

// 종합 보안 헤더 설정
app.use(helmet());

// Content Security Policy 설정
app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        scriptSrc: [
            "'self'",
            "'unsafe-inline'",
            "'unsafe-eval'",
            "https://cdn.jsdelivr.net",
            "https://unpkg.com",
        ],
        styleSrc: [
            "'self'",
            "'unsafe-inline'",
            "https://fonts.googleapis.com",
            "https://cdn.jsdelivr.net",
        ],
        imgSrc: [
            "'self'",
            "data:",
            "https:",
            "blob:",
        ],
        fontSrc: [
            "'self'",
            "https://fonts.gstatic.com",
            "https://cdn.jsdelivr.net",
        ],
        connectSrc: [
            "'self'",
            "https://api.myapp.com",
            "wss://ws.myapp.com",
        ],
        mediaSrc: ["'self'"],
        objectSrc: ["'none'"],
        frameSrc: ["'none'"],
        baseUri: ["'self'"],
        formAction: ["'self'"],
        upgradeInsecureRequests: [],
    },
}));

// HSTS 설정
app.use(helmet.hsts({
    maxAge: 31536000, // 1년
    includeSubDomains: true,
    preload: true,
}));

// X-Content-Type-Options
app.use(helmet.noSniff());

// X-Frame-Options
app.use(helmet.frameguard({ action: 'deny' }));

// X-XSS-Protection (레거시 브라우저)
app.use(helmet.xssFilter());

// Referrer Policy
app.use(helmet.referrerPolicy({ policy: 'strict-origin-when-cross-origin' }));

// Permissions Policy
app.use(helmet.permissionsPolicy({
    features: {
        camera: [],
        microphone: [],
        geolocation: [],
        payment: [],
    },
}));

3. 환경별 설정

const helmetConfig = {
    development: {
        contentSecurityPolicy: false, // 개발 시 비활성화
        hsts: false,
    },
    production: {
        contentSecurityPolicy: {
            directives: {
                defaultSrc: ["'self'"],
                scriptSrc: ["'self'"],
                styleSrc: ["'self'", "'unsafe-inline'"],
                imgSrc: ["'self'", "data:", "https:"],
                connectSrc: ["'self'", "https://api.myapp.com"],
                fontSrc: ["'self'", "https://fonts.gstatic.com"],
                objectSrc: ["'none'"],
                frameAncestors: ["'none'"],
                baseUri: ["'self'"],
                formAction: ["'self'"],
            },
        },
        hsts: {
            maxAge: 31536000,
            includeSubDomains: true,
            preload: true,
        },
        referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
    },
};

const env = process.env.NODE_ENV || 'development';
app.use(helmet(helmetConfig[env]));

4. CSP 리포팅 설정

app.use(helmet.contentSecurityPolicy({
    directives: {
        defaultSrc: ["'self'"],
        reportUri: '/csp-report',
        reportTo: 'csp-endpoint',
    },
}));

// CSP 리포트 엔드포인트
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
    const report = req.body['csp-report'];
    console.error('CSP Violation:', {
        'blocked-uri': report['blocked-uri'],
        'violated-directive': report['violated-directive'],
        'effective-directive': report['effective-directive'],
        'original-policy': report['original-policy'],
    });
    res.status(204).end();
});

5. 커스텀 헤더 추가

// 커스텀 보안 헤더
app.use((req, res, next) => {
    // API 키 보호
    res.setHeader('X-Api-Version', '1.0');

    // 캐시 제어
    res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate');
    res.setHeader('Pragma', 'no-cache');

    // 추가 보안 헤더
    res.setHeader('X-Permitted-Cross-Domain-Policies', 'none');
    res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
    res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
    res.setHeader('Cross-Origin-Resource-Policy', 'same-origin');

    next();
});

Lessons Learned

  1. CSP 설정: XSS 공격 방지를 위해 Content-Security-Policy를 반드시 설정하세요
  2. HSTS 적용: HTTPS를 사용하는 경우 HSTS를 설정하여 보안을 강화하세요
  3. 환경별 설정: 개발과 프로덕션 환경에 따라 보안 헤더를 다르게 설정하세요
  4. CSP 리포팅: CSP 위반 사항을 모니터링하여 보안 문제를 조기에 발견하세요
  5. 정기 검사: security-headers-cli와 같은 도구로 보안 헤더를 정기적으로 검사하세요

This blog does not accept any external sponsorships, affiliate marketing, or ad revenue.