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 방어
 
파일 업로드 보안
- 파일 형식 검증
 - 크기 제한
 - 안전한 저장소 사용
 
데이터 보호
- 민감한 데이터 암호화
 - 안전한 키 관리
 - 전송 중 데이터 보호
 
모니터링과 로깅
- 보안 이벤트 로깅
 - 실시간 모니터링
 - 알림 시스템