Node.js 보안 설정: 실서비스에서 반드시 체크해야 할 핵심 가이드(Express 기준)
대표이미지: 서버 보안/네트워크를 상징하는 이미지 (URL 교체 가능)
Node.js 보안 설정: 실서비스에서 반드시 체크해야 할 핵심 가이드(Express 기준)
Node.js는 빠르게 서비스 만들기 좋지만, 기본 설정 그대로 배포하면 취약점이 그대로 노출되는 경우가 정말 많습니다. 특히 Express 기반 서버는 “설정 몇 줄” 차이로 공격 난이도가 확 달라집니다.
이 글은 실제 배포 환경에서 바로 적용 가능한 보안 체크리스트 형태로 정리했습니다. 단순 이론이 아니라, “어떤 공격을 막는지”와 “어떻게 코드로 막는지”를 같이 제공합니다.
1) 기본 원칙: 보안은 ‘기능’이 아니라 ‘기본 설정’이다
✅ 최소 권한(Least Privilege)
- DB 계정은 admin 금지 (읽기/쓰기 권한만 필요한 테이블에만 권한 부여)
- 서버 프로세스는 root 실행 금지 (Docker도 rootless 권장)
- API 키/토큰은 필요한 범위(scope)로만 발급
✅ 보안 기본값(Secure by Default)
- 모든 입력값 검증(Validation) 없이는 DB/파일/OS 호출 금지
- 쿠키/세션/토큰은 “기본값”으로 두지 말고 명시적으로 강화
- 에러 메시지는 사용자에게 최소만 노출
2) Express 보안 필수 미들웨어 5종 세트
실무에서 가장 많이 쓰는 조합입니다. “일단 이것부터” 적용하면 체감이 큽니다.
2-1. 보안 헤더: Helmet
npm i helmet
import express from "express";
import helmet from "helmet";
const app = express();
app.use(helmet()); // 기본 보안 헤더 세트 적용
Helmet은 XSS/클릭재킹/콘텐츠 스니핑 등 다양한 공격에 대한 기본 방어 헤더를 설정해 줍니다. 단, CSP(Content-Security-Policy)는 서비스 구조에 따라 커스터마이징이 필요할 수 있어요.
2-2. CORS: 허용 도메인 화이트리스트
npm i cors
import cors from "cors";
const allowlist = [
"https://yourdomain.com",
"https://www.yourdomain.com",
];
app.use(cors({
origin: (origin, cb) => {
// 서버-서버 호출/로컬 테스트 등 origin이 없을 수도 있음
if (!origin) return cb(null, true);
if (allowlist.includes(origin)) return cb(null, true);
return cb(new Error("Not allowed by CORS"));
},
credentials: true,
methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
}));
CORS를 *로 두는 순간 “아무 사이트에서나” API를 브라우저로 호출할 수 있게 됩니다. 프론트 도메인이 정해져 있다면 반드시 화이트리스트로 관리하세요.
2-3. 요청 폭주 방지: Rate Limit
npm i express-rate-limit
import rateLimit from "express-rate-limit";
// 로그인/인증 같은 민감 엔드포인트는 더 강하게 제한 권장
const authLimiter = rateLimit({
windowMs: 10 * 60 * 1000, // 10분
limit: 50, // IP당 50회
standardHeaders: true,
legacyHeaders: false,
message: { message: "Too many requests. Please try again later." },
});
app.use("/api/auth", authLimiter);
브루트포스(비밀번호 대입) 공격, 무차별 스캔, 크롤링 폭주를 초반에 잘라줍니다. 특히 로그인/회원가입/비밀번호 재설정은 반드시 별도 제한을 두세요.
2-4. 바디 크기 제한: DOS 방지
app.use(express.json({ limit: "200kb" }));
app.use(express.urlencoded({ extended: true, limit: "200kb" }));
큰 payload를 무한히 던져 서버 메모리를 터뜨리는 공격이 가능합니다. 파일 업로드는 별도 경로로 분리하고, 일반 API는 100~300kb 수준으로 제한하는 편이 안전합니다.
2-5. 파라미터 오염/프로토타입 오염 방지
npm i hpp
import hpp from "hpp";
// a=1&a=2 같은 파라미터 오염 공격 방어
app.use(hpp());
3) 입력값 검증(Validation) + 정규화(Sanitize)는 “선택”이 아니다
SQL 인젝션은 ORM으로 어느 정도 줄어도, NoSQL 인젝션, XSS, 명령 주입은 여전히 터집니다. 핵심은 “DB/파일/외부요청으로 들어가기 전에” 입력을 강제하는 것입니다.
3-1. Zod로 DTO 검증(추천)
npm i zod
import { z } from "zod";
const signupSchema = z.object({
email: z.string().email(),
password: z.string().min(10).max(72), // bcrypt 고려: 72자 권장
name: z.string().min(2).max(30),
});
app.post("/api/auth/signup", async (req, res) => {
const parsed = signupSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
message: "Invalid input",
errors: parsed.error.flatten(),
});
}
const { email, password, name } = parsed.data;
// 안전한 값만 여기부터 사용
return res.json({ ok: true });
});
3-2. XSS를 막는 핵심 포인트
- 서버가 HTML을 렌더링한다면: 템플릿 엔진에서 반드시 escape 처리
- API 서버라면: HTML 자체를 저장/반환하는 요구가 없으면 입력에서 태그 제거/거부
- 프론트에서 dangerouslySetInnerHTML 같은 기능은 최대한 금지
4) 인증/인가 설계: JWT를 쓰더라도 “보안 기본기”가 우선
4-1. 비밀번호 저장: bcrypt(또는 argon2) + 정책
npm i bcrypt
import bcrypt from "bcrypt";
const SALT_ROUNDS = 12;
export async function hashPassword(plain) {
return bcrypt.hash(plain, SALT_ROUNDS);
}
export async function verifyPassword(plain, hashed) {
return bcrypt.compare(plain, hashed);
}
- 최소 10자 이상 권장 (서비스 성격에 따라 강화)
- 재사용 비밀번호/유출 비밀번호 차단(가능하면) 고려
- 로그인 실패 횟수 제한 + 지연(Delay) 적용 권장
4-2. JWT 보안 체크리스트
- 만료시간(exp) 짧게: 액세스 토큰은 5~30분 권장
- 리프레시 토큰은 DB/Redis에 저장하고 로테이션(회전) 적용
- 알고리즘 고정(alg none 취약점류 방지), 키는 강력하게
- 토큰을 localStorage에 두는 설계는 XSS에 취약 → 가능하면 HttpOnly 쿠키 기반 고려
4-3. 쿠키로 운영한다면: HttpOnly + Secure + SameSite
res.cookie("access_token", token, {
httpOnly: true,
secure: true, // HTTPS 필수
sameSite: "lax", // 서비스 구조에 맞게 lax/strict/none 조정
maxAge: 15 * 60 * 1000,
path: "/",
});
쿠키 기반은 XSS에 상대적으로 강하지만, CSRF 고려가 필요합니다. SameSite 설정과 더불어 민감 요청에 CSRF 토큰을 섞거나, “동작 API는 무조건 Authorization 헤더” 같은 정책도 방법입니다.
5) 에러/로그: “개발 편의”가 “정보 유출”로 바뀌는 지점
5-1. 프로덕션 에러 메시지 최소화
app.use((err, req, res, next) => {
// 서버 내부에서는 상세 로그
console.error(err);
// 클라이언트에게는 최소 정보만
res.status(500).json({ message: "Internal Server Error" });
});
5-2. 로그에 절대 남기면 안 되는 것
- 비밀번호, 인증코드(OTP), 리프레시 토큰, 세션 ID
- Authorization 헤더 전체
- 주민번호/계좌번호/카드번호 등 민감정보
“디버깅용”으로 남긴 로그가 나중에 로그 서버/모니터링 툴에서 그대로 열람되며 사고가 납니다. 민감정보 마스킹은 초기에 습관으로 박아두는 게 좋습니다.
6) 환경변수(.env) & 시크릿 관리: 유출 사고 1순위
6-1. .env는 커밋 금지
# .gitignore
.env
.env.*
6-2. 시크릿 회전(Rotation) 전략
- 키 유출을 “막는 것”도 중요하지만, 유출 시 빠르게 교체할 수 있어야 함
- JWT 서명키/DB 비번/API 키는 주기적으로 변경 가능한 구조로
6-3. 런타임에서 필수 환경변수 강제
function must(key) {
const v = process.env[key];
if (!v) throw new Error(`Missing env: ${key}`);
return v;
}
export const ENV = {
NODE_ENV: process.env.NODE_ENV || "development",
JWT_SECRET: must("JWT_SECRET"),
DATABASE_URL: must("DATABASE_URL"),
};
7) HTTPS / 프록시 / 배포: “코드는 안전한데 운영이 뚫리는” 케이스
7-1. 프록시 뒤에서 secure 쿠키/클라이언트 IP 정확히 받기
app.set("trust proxy", 1); // Nginx/Cloudflare/Load Balancer 뒤라면 중요
프록시 환경에서 trust proxy 설정이 없으면 IP 기반 rate limit이 깨지거나, secure 쿠키 판단이 꼬일 수 있습니다.
7-2. HTTPS는 선택이 아니라 필수
- 로그인/결제/개인정보가 없더라도 HTTPS 기본
- Cloudflare/Nginx/ALB 등으로 TLS 종단 처리 권장
- HSTS(Helmet에서 옵션으로 가능)도 상황에 따라 적용
8) 의존성 보안: npm 취약점은 “시간차 폭탄”이다
8-1. audit 자동화
npm audit
npm audit fix
8-2. 잠금파일(lock)과 버전 전략
- package-lock.json / pnpm-lock.yaml은 반드시 커밋
- CI에서 정기적으로 audit 수행
- 중요 라이브러리는 메이저 업그레이드 시 변경점 검토
8-3. 불필요한 패키지 제거
설치된 패키지가 많아질수록 공격 표면이 넓어집니다. “편하니까” 넣었던 패키지가 취약점의 원인이 되는 경우가 흔합니다.
9) 실전 체크리스트(배포 전 10분 점검)
- [ ] Helmet 적용
- [ ] CORS 화이트리스트 + credentials 정책 정리
- [ ] Rate Limit(특히 인증 API)
- [ ] Body limit 설정
- [ ] 입력값 검증(Zod 등) 전 엔드포인트에 적용
- [ ] 에러 메시지 최소화(프로덕션)
- [ ] 로그 민감정보 마스킹
- [ ] .env 커밋 차단 + 시크릿 강제 로딩
- [ ] HTTPS + trust proxy 설정 확인
- [ ] npm audit / 의존성 최신화
10) 예시: “보안 기본 세팅”이 들어간 Express 템플릿
import express from "express";
import helmet from "helmet";
import cors from "cors";
import hpp from "hpp";
import rateLimit from "express-rate-limit";
const app = express();
// 1) 프록시 환경(Cloudflare/Nginx/ALB) 대응
app.set("trust proxy", 1);
// 2) 기본 보안 헤더
app.use(helmet());
// 3) 파라미터 오염 방지
app.use(hpp());
// 4) 바디 제한(DOS 방지)
app.use(express.json({ limit: "200kb" }));
app.use(express.urlencoded({ extended: true, limit: "200kb" }));
// 5) CORS 화이트리스트
const allowlist = ["https://yourdomain.com", "https://www.yourdomain.com"];
app.use(cors({
origin: (origin, cb) => {
if (!origin) return cb(null, true);
if (allowlist.includes(origin)) return cb(null, true);
return cb(new Error("Not allowed by CORS"));
},
credentials: true,
}));
// 6) 전역 Rate Limit (서비스 상황에 맞게 조정)
app.use(rateLimit({
windowMs: 60 * 1000,
limit: 300,
standardHeaders: true,
legacyHeaders: false,
}));
// 7) 헬스 체크
app.get("/health", (req, res) => res.json({ ok: true }));
// 8) 에러 핸들러(프로덕션 정보 최소화)
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ message: "Internal Server Error" });
});
app.listen(3000, () => console.log("Server listening on 3000"));
마무리
Node.js 보안은 “특별한 해킹 기술”보다 기본 설정이 더 중요합니다. Helmet/CORS/RateLimit/Validation/HTTPS/Secrets 이 6가지만 제대로 잡아도, 대부분의 흔한 공격은 초반에 차단됩니다.
다음 글에서는 더 실전으로, JWT 리프레시 토큰 로테이션, 권한(Role) 기반 인가, Redis로 세션/토큰 관리, Nginx 보안 설정까지 이어서 정리해보겠습니다.
관련 태그
#Nodejs보안 #Express보안 #Helmet #CORS설정 #RateLimit #입력값검증 #JWT보안 #HTTPS #환경변수보안 #웹서버보안
'it' 카테고리의 다른 글
| 개인정보 처리 방침 (0) | 2026.02.04 |
|---|---|
| 소개 및 문의 (0) | 2026.02.04 |
| 면책 조항 (0) | 2026.02.04 |
| 웹 취약점 점검 가이드 (실무 체크리스트 + 보고서 템플릿) (0) | 2026.02.04 |
| 개인 클라우드 서버 만들기: 집에서 나만의 드라이브·사진·백업을 운영하는 실전 가이드 (0) | 2026.02.04 |
| Node.js 서버 이메일 인증 구현 완성본 (Express + Prisma + PostgreSQL + Nodemailer) (0) | 2026.02.03 |
| 비전공자 네트워크 공부 독학 로드맵: “개념 → 실습 → 트러블슈팅”으로 끝내기 (0) | 2026.02.02 |
| 라즈베리파이 5 NAS 구축 가이드 (집에서 쓰는 실전형 NAS) (1) | 2026.02.01 |