Node.js 인터뷰 질문 88
질문: Node.js 애플리케이션에서 보안을 구현하는 주요 방법과 일반적인 보안 위협에 대한 대응 방안에 대해 설명해주세요.
답변:
Node.js 애플리케이션의 보안은 매우 중요한 주제입니다. 주요 보안 위협과 이에 대한 대응 방안을 살펴보겠습니다.
1. 입력 검증과 살균
사용자 입력에 대한 적절한 검증과 살균은 가장 기본적인 보안 조치입니다.
const express = require("express");
const validator = require("validator");
const xss = require("xss");
const app = express();
app.use(express.json());
// 입력 검증 미들웨어
function validateUserInput(req, res, next) {
const { email, password, name } = req.body;
// 이메일 검증
if (!validator.isEmail(email)) {
return res.status(400).json({ error: "유효하지 않은 이메일 형식입니다" });
}
// 비밀번호 강도 검증
if (
!validator.isStrongPassword(password, {
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1,
})
) {
return res
.status(400)
.json({ error: "비밀번호가 충분히 강력하지 않습니다" });
}
// XSS 방지를 위한 입력 살균
req.body.name = xss(name);
next();
}
// SQL 인젝션 방지를 위한 파라미터 바인딩
async function getUserById(id) {
const query = "SELECT * FROM users WHERE id = ?";
return await db.query(query, [id]);
}
// 라우트에 검증 미들웨어 적용
app.post("/users", validateUserInput, async (req, res) => {
// 사용자 생성 로직
});
2. 인증과 권한 부여
안전한 인증과 권한 부여 시스템 구현:
const jwt = require("jsonwebtoken");
const bcrypt = require("bcrypt");
class AuthService {
constructor() {
this.secretKey = process.env.JWT_SECRET;
}
// 비밀번호 해싱
async hashPassword(password) {
const salt = await bcrypt.genSalt(10);
return await bcrypt.hash(password, salt);
}
// 비밀번호 검증
async verifyPassword(password, hash) {
return await bcrypt.compare(password, hash);
}
// JWT 토큰 생성
generateToken(user) {
return jwt.sign(
{
id: user.id,
role: user.role,
},
this.secretKey,
{ expiresIn: "1h" }
);
}
// 인증 미들웨어
authenticate(req, res, next) {
const token = req.headers.authorization?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "인증이 필요합니다" });
}
try {
const decoded = jwt.verify(token, this.secretKey);
req.user = decoded;
next();
} catch (error) {
return res.status(401).json({ error: "유효하지 않은 토큰입니다" });
}
}
// 권한 검사 미들웨어
authorize(roles = []) {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: "권한이 없습니다" });
}
next();
};
}
}
// 사용 예시
const authService = new AuthService();
app.post("/login", async (req, res) => {
const { email, password } = req.body;
try {
const user = await findUserByEmail(email);
if (!user) {
return res.status(401).json({ error: "사용자를 찾을 수 없습니다" });
}
const isValid = await authService.verifyPassword(password, user.password);
if (!isValid) {
return res.status(401).json({ error: "비밀번호가 일치하지 않습니다" });
}
const token = authService.generateToken(user);
res.json({ token });
} catch (error) {
res.status(500).json({ error: "로그인 처리 중 오류가 발생했습니다" });
}
});
// 보호된 라우트
app.get(
"/admin",
authService.authenticate,
authService.authorize(["admin"]),
(req, res) => {
res.json({ message: "관리자 페이지에 접근했습니다" });
}
);
3. 보안 헤더 설정
보안 관련 HTTP 헤더 설정:
const helmet = require("helmet");
const express = require("express");
const app = express();
// 기본 보안 헤더 설정
app.use(helmet());
// CSP(Content Security Policy) 설정
app.use(
helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'", "https://api.example.com"],
},
})
);
// CORS 설정
const cors = require("cors");
app.use(
cors({
origin: ["https://example.com"],
methods: ["GET", "POST", "PUT", "DELETE"],
allowedHeaders: ["Content-Type", "Authorization"],
credentials: true,
})
);
4. 속도 제한과 DDoS 방어
서비스 거부 공격 방지:
const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis");
const Redis = require("ioredis");
const redis = new Redis({
host: "localhost",
port: 6379,
});
// API 속도 제한 설정
const apiLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: "rate-limit:",
}),
windowMs: 15 * 60 * 1000, // 15분
max: 100, // IP당 최대 요청 수
message: "너무 많은 요청을 보냈습니다. 잠시 후 다시 시도해주세요.",
});
// 로그인 시도 제한
const loginLimiter = rateLimit({
store: new RedisStore({
client: redis,
prefix: "login-limit:",
}),
windowMs: 60 * 60 * 1000, // 1시간
max: 5, // IP당 최대 시도 횟수
message: "로그인 시도가 너무 많습니다. 1시간 후 다시 시도해주세요.",
});
app.use("/api/", apiLimiter);
app.use("/login", loginLimiter);
5. 파일 업로드 보안
안전한 파일 업로드 처리:
const multer = require("multer");
const path = require("path");
const crypto = require("crypto");
// 파일 업로드 설정
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, "uploads/");
},
filename: (req, file, cb) => {
// 안전한 파일명 생성
const randomName = crypto.randomBytes(16).toString("hex");
cb(null, `${randomName}${path.extname(file.originalname)}`);
},
});
// 파일 필터
const fileFilter = (req, file, cb) => {
// 허용된 파일 형식 검사
const allowedTypes = ["image/jpeg", "image/png", "image/gif"];
if (!allowedTypes.includes(file.mimetype)) {
cb(new Error("지원하지 않는 파일 형식입니다"), false);
return;
}
cb(null, true);
};
const upload = multer({
storage,
fileFilter,
limits: {
fileSize: 5 * 1024 * 1024, // 5MB
},
});
app.post("/upload", upload.single("file"), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: "파일이 없습니다" });
}
// 파일 처리 로직
res.json({
filename: req.file.filename,
path: req.file.path,
});
});
6. 암호화와 데이터 보호
민감한 데이터 암호화:
const crypto = require("crypto");
class Encryption {
constructor(encryptionKey) {
this.algorithm = "aes-256-gcm";
this.key = crypto.scryptSync(encryptionKey, "salt", 32);
}
// 데이터 암호화
encrypt(text) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv(this.algorithm, this.key, iv);
let encrypted = cipher.update(text, "utf8", "hex");
encrypted += cipher.final("hex");
const authTag = cipher.getAuthTag();
return {
iv: iv.toString("hex"),
encrypted: encrypted,
authTag: authTag.toString("hex"),
};
}
// 데이터 복호화
decrypt(encrypted) {
const decipher = crypto.createDecipheriv(
this.algorithm,
this.key,
Buffer.from(encrypted.iv, "hex")
);
decipher.setAuthTag(Buffer.from(encrypted.authTag, "hex"));
let decrypted = decipher.update(encrypted.encrypted, "hex", "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
}
// 사용 예시
const encryption = new Encryption(process.env.ENCRYPTION_KEY);
// 민감한 데이터 저장
async function saveUserData(userId, sensitiveData) {
const encrypted = encryption.encrypt(JSON.stringify(sensitiveData));
await db.query("UPDATE users SET sensitive_data = ? WHERE id = ?", [
JSON.stringify(encrypted),
userId,
]);
}
// 민감한 데이터 조회
async function getUserData(userId) {
const result = await db.query(
"SELECT sensitive_data FROM users WHERE id = ?",
[userId]
);
if (!result.length) return null;
const encrypted = JSON.parse(result[0].sensitive_data);
return JSON.parse(encryption.decrypt(encrypted));
}
7. 보안 모니터링과 로깅
보안 이벤트 모니터링과 로깅:
const winston = require("winston");
const { createLogger, format, transports } = winston;
// 보안 로거 설정
const securityLogger = createLogger({
format: format.combine(format.timestamp(), format.json()),
transports: [
new transports.File({
filename: "security.log",
level: "info",
}),
new transports.Console({
level: "warn",
}),
],
});
// 보안 이벤트 로깅 미들웨어
function securityLogging(req, res, next) {
// 요청 정보 로깅
securityLogger.info("보안 이벤트", {
timestamp: new Date(),
ip: req.ip,
method: req.method,
url: req.url,
headers: req.headers,
user: req.user?.id,
});
// 응답 완료 시 로깅
res.on("finish", () => {
if (res.statusCode >= 400) {
securityLogger.warn("보안 경고", {
statusCode: res.statusCode,
ip: req.ip,
method: req.method,
url: req.url,
});
}
});
next();
}
// 보안 이벤트 알림
function notifySecurityEvent(event) {
securityLogger.error("보안 위반", event);
// 심각한 보안 이벤트 발생 시 관리자에게 알림
if (event.severity === "high") {
notifyAdministrators(event);
}
}
app.use(securityLogging);
요약
Node.js 애플리케이션의 주요 보안 고려사항:
입력 검증과 살균
- 모든 사용자 입력 검증
- XSS, SQL 인젝션 방지
- 데이터 타입과 형식 검증
인증과 권한 부여
- 안전한 패스워드 관리
- JWT 또는 세션 기반 인증
- 역할 기반 접근 제어
보안 헤더
- Helmet 사용
- CSP 설정
- CORS 설정
속도 제한
- API 요청 제한
- 브루트 포스 공격 방지
- DDoS 방어
파일 업로드 보안
- 파일 형식 검증
- 크기 제한
- 안전한 저장소 사용
데이터 보호
- 민감한 데이터 암호화
- 안전한 키 관리
- 전송 중 데이터 보호
모니터링과 로깅
- 보안 이벤트 로깅
- 실시간 모니터링
- 알림 시스템