Express 앱은 기본 설정만으로는 XSS, CSRF, SQL Injection 같은 공격에 무방비 상태다. 보안은 나중에 추가하는 옵션이 아니라 처음부터 설계에 포함해야 하는 요소다. 이 글은 실무에서 바로 적용할 수 있는 Express 보안 레이어를 단계별로 다룬다.
Client Request
│
▼
┌─────────────┐
│ Helmet │ ← HTTP 헤더 강화
└─────────────┘
│
▼
┌─────────────┐
│ CORS │ ← 출처 검증
└─────────────┘
│
▼
┌─────────────┐
│ Rate Limit │ ← 요청 수 제한
└─────────────┘
│
▼
┌─────────────┐
│ Sanitize │ ← 입력값 정제 (XSS/SQLi)
└─────────────┘
│
▼
Route Handler
Helmet은 보안 관련 HTTP 헤더를 자동으로 설정하는 Middleware다. X-Powered-By 노출 차단, Content-Security-Policy 설정 등을 한 번에 처리한다.
npm install helmet
import express from "express";
import helmet from "helmet";
const app = express();
app.use(
helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
},
})
);
CSP(Content-Security-Policy): 브라우저가 로드할 수 있는 리소스 출처를 제한해 XSS 공격을 방어하는 헤더.
출처를 명시적으로 허용하지 않으면 * 와일드카드 허용 상태가 되어 CSRF 공격에 취약해진다.
import cors from "cors";
const allowedOrigins = ["https://myapp.com", "https://admin.myapp.com"];
app.use(
cors({
origin: (origin, callback) => {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
})
);
credentials: true 설정 시 Access-Control-Allow-Origin에 *를 사용할 수 없으며, 반드시 명시적 출처를 지정해야 한다.
로그인 엔드포인트에 Rate Limit이 없으면 Brute Force 공격에 그대로 노출된다.
import rateLimit from "express-rate-limit";
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 10,
message: { error: "Too many login attempts. Try again later." },
standardHeaders: true,
legacyHeaders: false,
});
app.post("/auth/login", loginLimiter, loginController);
사용자 입력을 DB에 저장하기 전에 HTML 태그를 제거해야 한다.
import { JSDOM } from "jsdom";
import createDOMPurify from "dompurify";
const { window } = new JSDOM("");
const DOMPurify = createDOMPurify(window);
function sanitize(input: string): string {
return DOMPurify.sanitize(input, { ALLOWED_TAGS: [] });
}
app.post("/comments", (req, res) => {
const clean = sanitize(req.body.content);
// clean을 DB에 저장
});
문자열 접합으로 쿼리를 만드는 것은 가장 흔한 실수다. Parameterized Query를 반드시 사용해야 한다.
// 위험 — 절대 사용 금지
const query = `SELECT * FROM users WHERE email = '${email}'`;
// 안전 — Parameterized Query
const result = await db.query(
"SELECT * FROM users WHERE email = $1",
[email]
);
| 항목 | 도구 | 적용 범위 |
|---|---|---|
| HTTP 헤더 강화 | Helmet | 전역 Middleware |
| 출처 제한 | CORS | 전역 Middleware |
| 요청 수 제한 | express-rate-limit | 인증 엔드포인트 |
| XSS 정제 | DOMPurify | 사용자 입력 처리 |
| SQL Injection 방어 | Parameterized Query | 모든 DB 쿼리 |
| HTTPS 강제 | HSTS 헤더 (Helmet) | 프로덕션 전체 |
HSTS는 Helmet의 strictTransportSecurity 옵션으로 활성화되며, 브라우저가 이후 요청을 자동으로 HTTPS로 업그레이드하도록 강제한다.