it ·

Express 보안 미들웨어: 운영에서 바로 쓰는 필수 조합

반응형

<!doctype html>

Express 보안 미들웨어 대표 이미지
Express 보안 미들웨어 실전 구성 가이드

Express 보안 미들웨어: 운영에서 바로 쓰는 필수 조합

Express는 가볍고 유연한 대신, 기본값만으로는 “운영 보안”이 완성되지 않습니다. 그래서 보안은 라우트 코드에 흩뿌리는 게 아니라, 미들웨어 조합으로 표준화하는 게 정답입니다.

이 글에서는 “보안 헤더 + CORS + 요청 제한 + 입력 검증”을 중심으로 실제로 배포 환경에서 자주 터지는 포인트까지 포함해, 복붙 가능한 형태로 정리합니다.


1) Express 보안 미들웨어를 먼저 설계해야 하는 이유

1-1. 공격은 라우트가 아니라 “공통 계층”을 노립니다

운영 장애를 만드는 공격/실수는 대체로 특정 API 하나가 아니라, 서버 전체에 영향을 주는 형태로 들어옵니다. 예를 들면:

  • 무차별 로그인 시도(Brute Force) → 인증 서버/DB 과부하
  • 폭주 트래픽/봇 → 과금 증가 + 장애
  • 잘못된 CORS 설정 → 프론트에서 “정상 요청인데 차단”
  • 대용량 Body 업로드 → 메모리/CPU 폭발
  • 중복 쿼리 파라미터(HTTP Parameter Pollution) → 권한 우회/로직 꼬임

1-2. “미들웨어 표준 세트”로 팀 생산성이 올라갑니다

보안 코드를 라우트마다 작성하면, 누락/중복/정책 불일치가 생깁니다. 반면 app.use() 단계에서 정책을 통일하면, 새 기능을 추가해도 보안 수준이 유지됩니다.


2) 운영 기본 보안 체크리스트(Express 공통)

2-1. Express 기본 옵션

  • X-Powered-By 비활성화 (프레임워크 노출 최소화)
  • Body 크기 제한 (JSON/폼 데이터 모두)
  • trust proxy 설정 (프록시/로드밸런서 환경에서 IP 인식)
  • 에러 응답 표준화 (스택트레이스/내부 정보 노출 방지)

2-2. “보안 미들웨어 5종 세트” 추천

  • helmet : 보안 헤더 표준(Clickjacking/MIME sniffing 등)
  • cors : 허용 Origin을 명시적으로 통제
  • express-rate-limit : IP 기반 요청 제한
  • hpp : HTTP Parameter Pollution 방어
  • 입력 검증(Validation) : zod/joi/express-validator로 스키마 검증

3) 실전 코드: 한 번에 적용하는 보안 미들웨어 구성

아래 코드는 “API 서버” 기준으로 안전한 기본값을 구성했습니다. 프론트엔드(React/Next/Vue 등)와 통신하는 환경을 가정하며, 필요 시 옵션만 조정하면 됩니다.

3-1. 설치

npm i helmet cors express-rate-limit hpp
# 입력 검증(택1)
npm i zod
# 또는
npm i express-validator

3-2. app.js / server.js 예시(운영 기본형)

// server.js
import express from "express";
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";
import hpp from "hpp";

const app = express();

// 1) 프레임워크 노출 최소화
app.disable("x-powered-by");

// 2) 프록시(Cloudflare/Nginx/ELB 등) 뒤에 있다면 trust proxy 설정이 중요합니다.
//    - 1: "바로 앞의 프록시 1개"를 신뢰
//    - 환경에 따라 숫자 조정
app.set("trust proxy", 1);

// 3) Body 파서 + 크기 제한 (대부분의 장애는 여기서 시작합니다)
app.use(express.json({ limit: "200kb" }));
app.use(express.urlencoded({ extended: true, limit: "200kb" }));

// 4) 보안 헤더 (기본형)
app.use(
  helmet({
    // API 서버가 CDN/프론트 도메인과 함께 리소스를 쓰는 경우 CSP는 정책 설계 후 켜는 것을 권장
    // contentSecurityPolicy: false,
  })
);

// 5) CORS (정확히 허용할 Origin만)
const ALLOW_ORIGINS = [
  "https://example.com",
  "https://www.example.com",
  "http://localhost:3000",
];

app.use(
  cors({
    origin(origin, cb) {
      // 같은 도메인/서버-서버 호출처럼 Origin이 없는 경우도 있으니 허용 정책을 명확히 결정하세요.
      if (!origin) return cb(null, true);

      if (ALLOW_ORIGINS.includes(origin)) return cb(null, true);

      return cb(new Error("CORS blocked: not allowed origin"), false);
    },
    credentials: true, // 쿠키 기반 인증이면 true
    methods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"],
    allowedHeaders: ["Content-Type", "Authorization"],
    maxAge: 600,
  })
);

// 6) Rate Limit (로그인/인증/비싼 API는 별도 limiter 권장)
const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1분
  max: 120,            // IP당 1분에 120회
  standardHeaders: true,
  legacyHeaders: false,
  message: { message: "Too many requests" },
});

app.use("/api", apiLimiter);

// 로그인/인증 전용 (더 엄격하게)
const authLimiter = rateLimit({
  windowMs: 10 * 60 * 1000, // 10분
  max: 20,
  standardHeaders: true,
  legacyHeaders: false,
  message: { message: "Too many auth attempts" },
});
app.use("/api/auth", authLimiter);

// 7) HPP 방어 (중복 파라미터로 로직 꼬이는 것 방지)
app.use(
  hpp({
    // 화이트리스트가 필요한 경우만 추가
    // whitelist: ["tag", "id"]
  })
);

// 8) 라우트 예시
app.get("/api/health", (req, res) => {
  res.json({ ok: true });
});

// 9) 404 핸들러
app.use((req, res) => {
  res.status(404).json({ message: "Not Found" });
});

// 10) 에러 핸들러 (내부 정보 노출 방지)
app.use((err, req, res, next) => {
  // 운영에서는 err.stack을 그대로 보내지 마세요.
  const status = err.statusCode || 500;
  res.status(status).json({
    message: err.message || "Internal Server Error",
  });
});

app.listen(3000, () => {
  console.log("Server running on http://localhost:3000");
});

4) 보안 미들웨어별 “운영에서 자주 망하는 포인트”

4-1. Helmet: 켜는 것만으로 끝이 아닙니다

Helmet은 기본적으로 “보안 헤더를 켜주는 역할”이지만, 운영에서 정말 강력해지는 지점은 CSP(Content-Security-Policy)입니다. 다만 CSP는 잘못 켜면 프론트 리소스(스크립트/이미지)가 깨지므로, 아래 방식으로 접근하는 걸 추천합니다.

  • 초기: helmet 기본값 적용 (대부분 안전)
  • 운영 안정화 후: CSP 정책을 단계적으로 강화

4-2. CORS: allow-all(*)이 가장 위험합니다

“일단 되게 하자”로 origin: "*"를 켜면, 인증(쿠키/세션)과 결합될 때 보안 사고로 이어질 수 있습니다. 보통 운영에서는 아래 원칙을 씁니다.

  • 허용 Origin 목록 화이트리스트
  • credentials(true)는 쿠키 인증일 때만
  • Authorization 헤더를 쓰는 토큰 방식이면, 서버-클라 규칙을 명확히

4-3. Rate Limit: trust proxy 설정을 안 하면 “전원 차단”이 됩니다

로드밸런서/프록시 뒤에 있는 서버는 실제 사용자 IP가 아니라 프록시 IP로 보이는 경우가 많습니다. 이 상태로 rate-limit을 걸면, 특정 임계치를 넘는 순간 전체 사용자가 한꺼번에 차단될 수 있습니다.

  • app.set("trust proxy", 1) 등 환경에 맞게 설정
  • 비싼 API / 로그인 API는 별도 limiter로 더 강하게
  • 가능하면 “IP + 사용자 식별자(로그인 ID 등)” 기준을 혼합

4-4. HPP: “중복 쿼리”는 생각보다 자주 터집니다

Express는 같은 이름의 쿼리 파라미터가 들어오면 배열로 처리할 수 있어, 개발자가 의도하지 않은 형태로 로직이 흘러갈 수 있습니다. 예: /pay?price=100&price=1 같은 요청이 들어올 때, 코드를 잘못 짜면 우회가 발생할 수 있습니다.


5) “XSS 필터 미들웨어”에 대한 현실적인 조언

5-1. 결론: 무작정 sanitize 미들웨어 하나로 해결되지 않습니다

예전에는 “요청값 전체를 sanitize 해주는 미들웨어”를 쉽게 붙였지만, 최근에는 유지보수/정확성/부작용 이슈로 인해 단순 해결책이 되기 어렵습니다. 운영에서 더 안전한 접근은 다음입니다.

  • 입력 검증(Validation)으로 형태/길이/패턴을 제한
  • 출력 시점에 이스케이프(Output Encoding)를 철저히
  • HTML을 저장/렌더링해야 한다면 sanitize-html 같은 “목적형” 정책 사용

5-2. 입력 검증을 “보안 미들웨어”로 격상시키기

제일 재현 가능하고 팀에서 유지 가능한 방식이 스키마 검증입니다. 예시는 zod 기준으로 보여드릴게요.

// validators.js
import { z } from "zod";

export const createPostSchema = z.object({
  title: z.string().min(1).max(80),
  content: z.string().min(1).max(5000),
  tags: z.array(z.string().min(1).max(20)).max(10).optional(),
});
// middleware/validate.js
export function validate(schema) {
  return (req, res, next) => {
    try {
      // body만 검증하는 예시 (query/params도 동일 방식으로 확장 가능)
      req.body = schema.parse(req.body);
      next();
    } catch (e) {
      return res.status(400).json({
        message: "Invalid request",
        // 운영에서는 세부 에러를 숨기고, 개발 환경에서만 상세 출력하는 것을 권장
      });
    }
  };
}
// route example
import express from "express";
import { createPostSchema } from "./validators.js";
import { validate } from "./middleware/validate.js";

const router = express.Router();

router.post("/api/posts", validate(createPostSchema), (req, res) => {
  // 여기부터는 req.body가 “검증된 형태”라는 전제 하에 안전하게 처리
  res.json({ ok: true, data: req.body });
});

export default router;

6) 추가로 붙이면 좋은 보안 미들웨어/패턴

6-1. 요청/응답 로깅(민감정보 마스킹 필수)

  • 운영 장애의 대부분은 “로그가 없어서” 복구가 늦어집니다.
  • 단, Authorization/쿠키/비밀번호 같은 민감정보는 반드시 마스킹하세요.

6-2. 업로드/멀티파트 제한

  • 파일 업로드는 DoS 표적이 되기 쉬워 크기/확장자/개수 제한이 필수입니다.
  • 업로드 처리 미들웨어는 “버전 업데이트”와 “예외 처리”가 핵심입니다.

6-3. 인증 방식에 따른 CSRF 전략

  • 쿠키 기반 세션: CSRF 대응을 고려해야 합니다.
  • Authorization Bearer 토큰: CSRF 위험이 상대적으로 낮지만, 토큰 탈취 방지가 더 중요합니다.

7) 최종 점검 체크리스트(복붙용)

  • [ ] app.disable("x-powered-by") 적용
  • [ ] express.json / urlencoded limit 설정
  • [ ] helmet 적용
  • [ ] CORS 화이트리스트 적용 + credentials 정책 확정
  • [ ] /api rate-limit + /api/auth 전용 rate-limit 분리
  • [ ] trust proxy 환경에 맞게 설정
  • [ ] hpp 적용(필요 시 whitelist)
  • [ ] 입력 검증(zod/joi 등) 적용
  • [ ] 에러 핸들러에서 내부 정보 노출 방지
  • [ ] 운영 로그에 민감정보 마스킹

마무리

Express 보안은 “라이브러리 몇 개 설치”가 아니라, 정책(허용/차단 기준) + 표준 미들웨어 조합으로 완성됩니다. 위 구성만 제대로 잡아도 운영에서 흔히 겪는 트래픽 폭주, 인증 공격, 설정 실수를 크게 줄일 수 있습니다.

Express 보안 미들웨어(Helmet, CORS, express-rate-limit, hpp)를 운영에서 바로 적용하는 실전 가이드. trust proxy, 요청 제한, 입력 검증까지 복붙 코드로 정리.

태그 : Express, Node.js, 보안, 미들웨어, Helmet, CORS, RateLimit, hpp, API보안, 입력검증

반응형