it ·

Node.js 보안 설정: 실서비스에서 반드시 체크해야 할 핵심 가이드(Express 기준)

728x90
반응형
Node.js 보안 설정 - 서버 보안과 인증/인가

대표이미지: 서버 보안/네트워크를 상징하는 이미지 (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 보안 설정까지 이어서 정리해보겠습니다.


Node.js(Express) 실서비스 보안 설정 총정리: Helmet, CORS 화이트리스트, Rate Limit, 입력값 검증(Zod), HTTPS/프록시, 로그/에러 처리, .env 시크릿 관리까지 한 번에 점검하세요.

관련 태그

#Nodejs보안 #Express보안 #Helmet #CORS설정 #RateLimit #입력값검증 #JWT보안 #HTTPS #환경변수보안 #웹서버보안

728x90
반응형