Express 보안 미들웨어: 운영에서 바로 쓰는 필수 조합
<!doctype html>
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, Node.js, 보안, 미들웨어, Helmet, CORS, RateLimit, hpp, API보안, 입력검증
'it' 카테고리의 다른 글
| 초보자를 위한 Python Selenium 환경 구축 가이드 (2026년 최신판) (0) | 2026.02.10 |
|---|---|
| OSI 7계층 실무 사례: 장애 원인 10분 안에 좁히는 사고방식 (0) | 2026.02.10 |
| 자바 17/21 마이그레이션 가이드 (실무 체크리스트 + 트러블슈팅) (0) | 2026.02.09 |
| Spring Boot 3 최신 특징 (2026 기준으로 “지금” 꼭 알아야 할 변화들) (0) | 2026.02.08 |
| Spring Boot 3.x + JPA로 게시판 만들기 (가장 쉬운 입문 가이드) (0) | 2026.02.06 |
| 엑셀 Copilot으로 복잡한 수식 1초 만에 만드는 법 (실무 프롬프트 템플릿 포함) (0) | 2026.02.06 |
| 라즈베리파이 5로 만드는 나만의 개인 클라우드 (Nextcloud 구축) (0) | 2026.02.06 |
| Node.js 가비아 호스팅 배포 시 발생하는 흔한 오류와 해결법 (0) | 2026.02.06 |