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 애플리케이션의 주요 보안 고려사항:

  1. 입력 검증과 살균

    • 모든 사용자 입력 검증
    • XSS, SQL 인젝션 방지
    • 데이터 타입과 형식 검증
  2. 인증과 권한 부여

    • 안전한 패스워드 관리
    • JWT 또는 세션 기반 인증
    • 역할 기반 접근 제어
  3. 보안 헤더

    • Helmet 사용
    • CSP 설정
    • CORS 설정
  4. 속도 제한

    • API 요청 제한
    • 브루트 포스 공격 방지
    • DDoS 방어
  5. 파일 업로드 보안

    • 파일 형식 검증
    • 크기 제한
    • 안전한 저장소 사용
  6. 데이터 보호

    • 민감한 데이터 암호화
    • 안전한 키 관리
    • 전송 중 데이터 보호
  7. 모니터링과 로깅

    • 보안 이벤트 로깅
    • 실시간 모니터링
    • 알림 시스템

results matching ""

    No results matching ""