Node.js 인터뷰 질문 58

질문: Node.js 애플리케이션의 주요 보안 취약점과 이를 방지하기 위한 모범 사례는 무엇인가요?

답변:

Node.js 애플리케이션은 웹 서비스나 API를 구축할 때 널리 사용되지만, 보안에 주의를 기울이지 않으면 다양한 취약점에 노출될 수 있습니다. 주요 보안 취약점과 이를 방지하기 위한 모범 사례에 대해 알아보겠습니다.

1. 주요 보안 취약점

1.1 인젝션 공격 (Injection Attacks)

SQL 인젝션, NoSQL 인젝션, 명령어 인젝션 등이 이에 해당합니다.

// SQL 인젝션에 취약한 코드
const query = `SELECT * FROM users WHERE username = '${req.body.username}'`;
db.query(query);

1.2 크로스 사이트 스크립팅 (XSS)

사용자 입력이 적절히 이스케이프되지 않고 클라이언트에게 반환될 때 발생합니다.

// XSS에 취약한 코드
app.get("/profile", (req, res) => {
  res.send(`<div>Welcome, ${req.query.name}!</div>`);
});

1.3 취약한 의존성 (Vulnerable Dependencies)

오래되거나 보안 취약점이 있는 npm 패키지를 사용할 때 발생합니다.

1.4 부적절한 인증 및 권한 부여

사용자 인증과 권한 검사가 부적절하게 구현될 때 발생합니다.

1.5 민감한 데이터 노출

암호화 없이 민감한 정보를 저장하거나 전송할 때 발생합니다.

1.6 보안 설정 오류

기본 설정 그대로 사용하거나 운영 환경에서 디버그 모드를 활성화하는 등의 문제가 여기에 해당합니다.

2. 인젝션 공격 방지

2.1 SQL 인젝션 방지

매개변수화된 쿼리나 ORM을 사용하여 방지합니다:

// 매개변수화된 쿼리 사용 (MySQL)
const query = "SELECT * FROM users WHERE username = ?";
db.query(query, [req.body.username]);

// Sequelize ORM 사용
const user = await User.findOne({
  where: { username: req.body.username },
});

2.2 NoSQL 인젝션 방지

데이터 타입 검증 및 변환을 통해 방지합니다:

// MongoDB 인젝션에 취약한 코드
const query = { username: req.body.username };
db.collection("users").find(query);

// 개선된 코드: 타입 검증 및 변환
const username = String(req.body.username);
const query = { username: username };
db.collection("users").find(query);

2.3 명령어 인젝션 방지

child_process.exec 대신 child_process.execFile을 사용하거나 shell 옵션을 비활성화합니다:

// 취약한 코드
const { exec } = require("child_process");
exec(`ping ${userInput}`); // 인젝션 위험

// 개선된 코드
const { execFile } = require("child_process");
execFile("ping", [userInput]); // 더 안전함

3. 크로스 사이트 스크립팅(XSS) 방지

3.1 입력 유효성 검사 및 출력 이스케이프

const escapeHtml = require("escape-html");

app.get("/profile", (req, res) => {
  // 사용자 입력 이스케이프
  const name = escapeHtml(req.query.name || "");
  res.send(`<div>Welcome, ${name}!</div>`);
});

3.2 Content Security Policy (CSP) 구현

const helmet = require("helmet");
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "trusted-cdn.com"],
      // 다른 지시문 추가
    },
  })
);

3.3 HttpOnly 및 Secure 쿠키 사용

app.use(
  session({
    secret: "your-secret-key",
    cookie: {
      httpOnly: true, // JavaScript에서 쿠키 접근 방지
      secure: true, // HTTPS에서만 쿠키 전송
      sameSite: "strict", // CSRF 방지
    },
  })
);

4. 의존성 관리

4.1 정기적인 의존성 취약점 검사

# npm audit으로 취약점 검사
npm audit

# 자동 수정 시도
npm audit fix

# 의존성 업데이트
npm update

4.2 package-lock.json 또는 yarn.lock 사용

정확한 버전의 종속성을 보장하기 위해 lock 파일을 사용합니다.

4.3 Snyk 또는 npm audit과 같은 도구를 CI/CD 파이프라인에 통합

# GitHub Actions 워크플로우 예시
name: Security Scan

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: 노드 설정
        uses: actions/setup-node@v2
        with:
          node-version: "14"
      - name: 의존성 설치
        run: npm ci
      - name: 보안 취약점 검사
        run: npm audit --audit-level=high

5. 인증 및 권한 부여

5.1 강력한 패스워드 정책 구현

const passwordValidator = require("password-validator");

const schema = new passwordValidator();
schema
  .is()
  .min(8) // 최소 8자
  .is()
  .max(100) // 최대 100자
  .has()
  .uppercase() // 대문자 포함
  .has()
  .lowercase() // 소문자 포함
  .has()
  .digits(2) // 최소 2개의 숫자
  .has()
  .not()
  .spaces() // 공백 금지
  .has()
  .symbols(1); // 최소 1개의 특수문자

// 사용 예
app.post("/register", (req, res) => {
  if (!schema.validate(req.body.password)) {
    return res
      .status(400)
      .json({ error: "보안 요구사항을 충족하지 않는 비밀번호" });
  }
  // 계속 진행...
});

5.2 비밀번호 해싱

const bcrypt = require("bcrypt");

// 비밀번호 해싱
async function hashPassword(password) {
  const saltRounds = 12;
  return await bcrypt.hash(password, saltRounds);
}

// 해시 검증
async function verifyPassword(password, hash) {
  return await bcrypt.compare(password, hash);
}

// 사용 예
app.post("/register", async (req, res) => {
  try {
    const hashedPassword = await hashPassword(req.body.password);
    // 해시된 비밀번호 저장
    // ...
  } catch (error) {
    // 오류 처리
  }
});

5.3 JWT 보안 강화

const jwt = require("jsonwebtoken");

// JWT 생성
function generateToken(user) {
  return jwt.sign({ userId: user.id }, process.env.JWT_SECRET, {
    expiresIn: "1h", // 짧은 만료 시간
    algorithm: "HS256", // 알고리즘 명시
  });
}

// JWT 검증
function verifyToken(token) {
  try {
    return jwt.verify(token, process.env.JWT_SECRET);
  } catch (error) {
    return null;
  }
}

5.4 API 속도 제한 구현

const rateLimit = require("express-rate-limit");

// 로그인 요청 제한
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 5, // 15분 동안 IP당 최대 5번 요청
  message: "너무 많은 로그인 시도, 나중에 다시 시도하세요.",
});

app.post("/login", loginLimiter, (req, res) => {
  // 로그인 로직
});

// 전체 API 요청 제한
const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1분
  max: 100, // 1분 동안 IP당 최대 100번 요청
  message: "너무 많은 요청, 나중에 다시 시도하세요.",
});

app.use("/api/", apiLimiter);

6. 민감한 데이터 보호

6.1 환경 변수 사용

// .env 파일에 시크릿 저장
// DB_PASSWORD=your-secure-password
// API_KEY=your-api-key

require("dotenv").config();

// 환경 변수 사용
const dbConfig = {
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
};

6.2 데이터 암호화

const crypto = require("crypto");

// 대칭 암호화 함수
function encrypt(text) {
  const algorithm = "aes-256-cbc";
  const key = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
  const iv = crypto.randomBytes(16);

  const cipher = crypto.createCipheriv(algorithm, key, iv);
  let encrypted = cipher.update(text, "utf8", "hex");
  encrypted += cipher.final("hex");

  return { iv: iv.toString("hex"), encryptedData: encrypted };
}

// 복호화 함수
function decrypt(encryptedObj) {
  const algorithm = "aes-256-cbc";
  const key = Buffer.from(process.env.ENCRYPTION_KEY, "hex");
  const iv = Buffer.from(encryptedObj.iv, "hex");

  const decipher = crypto.createDecipheriv(algorithm, key, iv);
  let decrypted = decipher.update(encryptedObj.encryptedData, "hex", "utf8");
  decrypted += decipher.final("utf8");

  return decrypted;
}

6.3 HTTPS 사용

const https = require("https");
const fs = require("fs");
const express = require("express");
const app = express();

// HTTPS 설정
const options = {
  key: fs.readFileSync("path/to/private-key.pem"),
  cert: fs.readFileSync("path/to/certificate.pem"),
};

// HTTPS 서버 생성
https.createServer(options, app).listen(443, () => {
  console.log("HTTPS 서버가 포트 443에서 실행 중입니다");
});

7. 보안 헤더 설정

7.1 Helmet 사용

const helmet = require("helmet");
const express = require("express");
const app = express();

// 기본 보안 헤더 설정
app.use(helmet());

// 커스텀 보안 헤더 설정
app.use(
  helmet({
    contentSecurityPolicy: {
      directives: {
        defaultSrc: ["'self'"],
        scriptSrc: ["'self'", "'unsafe-inline'", "trusted-scripts.com"],
        styleSrc: ["'self'", "trusted-styles.com"],
        imgSrc: ["'self'", "data:", "trusted-images.com"],
        connectSrc: ["'self'", "api.trusted-source.com"],
      },
    },
    referrerPolicy: { policy: "same-origin" },
    xFrameOptions: { action: "deny" },
  })
);

8. 오류 처리 및 로깅

8.1 안전한 오류 처리

// 404 핸들러
app.use((req, res, next) => {
  res.status(404).json({ error: "요청한 리소스를 찾을 수 없습니다" });
});

// 500 핸들러
app.use((err, req, res, next) => {
  // 개발 환경에서만 자세한 오류 표시
  const error =
    process.env.NODE_ENV === "production"
      ? "서버 오류가 발생했습니다"
      : err.message;

  // 오류 로깅
  console.error("서버 오류:", err);

  res.status(500).json({ error });
});

8.2 보안 로깅

const winston = require("winston");

// 로거 구성
const logger = winston.createLogger({
  level: "info",
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.json()
  ),
  transports: [
    // 파일에 로그 저장
    new winston.transports.File({ filename: "error.log", level: "error" }),
    new winston.transports.File({ filename: "combined.log" }),
  ],
});

// 프로덕션이 아닌 경우 콘솔에도 로그 출력
if (process.env.NODE_ENV !== "production") {
  logger.add(
    new winston.transports.Console({
      format: winston.format.simple(),
    })
  );
}

// 로그 사용 예
app.use((req, res, next) => {
  // 개인 정보 필터링
  const logData = {
    method: req.method,
    path: req.path,
    ip: req.ip,
    // 민감한 데이터 제외 (예: req.body.password)
  };

  logger.info("요청 수신", logData);
  next();
});

9. 입력 유효성 검사

9.1 Joi 검증 라이브러리 사용

const Joi = require("joi");

// 사용자 생성 스키마
const userSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(30).required(),
  email: Joi.string().email().required(),
  password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{8,30}$")).required(),
  age: Joi.number().integer().min(18).max(120),
});

// 미들웨어로 사용
app.post("/users", (req, res, next) => {
  const { error, value } = userSchema.validate(req.body);

  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }

  // 검증 통과 시 계속 진행
  req.validatedData = value;
  next();
});

9.2 Express-Validator 사용

const { body, validationResult } = require("express-validator");

app.post(
  "/users",
  // 유효성 검증 규칙
  [
    body("username").isAlphanumeric().isLength({ min: 3, max: 30 }),
    body("email").isEmail().normalizeEmail(),
    body("password")
      .isLength({ min: 8 })
      .matches(/^[a-zA-Z0-9]{8,30}$/),
    body("age").isInt({ min: 18, max: 120 }),
  ],
  // 검증 결과 처리
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    // 검증 통과 시 계속 진행
    // ...
  }
);

10. CSRF 공격 방지

10.1 CSRF 토큰 사용

const csrf = require("csurf");
const cookieParser = require("cookie-parser");

// CSRF 미들웨어 설정
app.use(cookieParser());
app.use(csrf({ cookie: true }));

// 폼에 CSRF 토큰 제공
app.get("/form", (req, res) => {
  res.render("form", { csrfToken: req.csrfToken() });
});

// CSRF 토큰 검증
app.post("/submit", (req, res) => {
  // csurf 미들웨어가 자동으로 토큰 검증
  // 유효하지 않은 토큰은 403 오류 발생
  res.send("양식 제출 성공");
});

// CSRF 오류 처리
app.use((err, req, res, next) => {
  if (err.code === "EBADCSRFTOKEN") {
    return res.status(403).json({ error: "CSRF 토큰이 유효하지 않습니다" });
  }
  next(err);
});

11. 보안 모니터링 및 감사

11.1 보안 로깅 및 모니터링

const express = require("express");
const morgan = require("morgan");
const winston = require("winston");
const app = express();

// HTTP 요청 로깅
app.use(
  morgan("combined", {
    stream: {
      write: (message) => {
        // Winston 로거로 로그 전달
        logger.info(message.trim());
      },
    },
    // 민감한 데이터 제외
    skip: (req, res) =>
      req.path === "/health" || req.path.startsWith("/static"),
  })
);

// 인증 시도 로깅
app.post("/login", (req, res) => {
  // 성공 여부에 따라 로깅
  const success = // 인증 로직
    logger.info("로그인 시도", {
      username: req.body.username,
      ip: req.ip,
      userAgent: req.headers["user-agent"],
      success,
    });

  // 로그인 로직 계속
});

11.2 보안 감사

// 관리자 작업 감사 로깅
function auditLog(userId, action, details) {
  const auditEntry = {
    userId,
    action,
    details,
    timestamp: new Date().toISOString(),
    ip: this.req.ip,
    userAgent: this.req.headers["user-agent"],
  };

  // 감사 로그 저장
  logger.info("관리자 작업", auditEntry);
  db.collection("audit_logs").insertOne(auditEntry);
}

// 사용 예
app.delete("/users/:id", isAdmin, (req, res) => {
  const userId = req.params.id;

  // 사용자 삭제 로직
  db.collection("users").deleteOne({ _id: userId });

  // 감사 로그 기록
  auditLog(req.user.id, "DELETE_USER", {
    deletedUserId: userId,
  });

  res.status(200).json({ message: "사용자가 삭제되었습니다" });
});

12. 코드 보안 검사 도구

12.1 ESLint 보안 규칙 설정

// .eslintrc.js
module.exports = {
  extends: ["eslint:recommended", "plugin:security/recommended"],
  plugins: ["security"],
  rules: {
    "security/detect-eval-with-expression": "error",
    "security/detect-non-literal-regexp": "error",
    "security/detect-non-literal-require": "error",
    "security/detect-possible-timing-attacks": "error",
  },
};

12.2 SonarQube 또는 Snyk와 같은 도구를 통한 정적 코드 분석

# Snyk CLI 사용
npm install -g snyk
snyk test

# CI/CD에 통합
snyk test --severity-threshold=high

13. 컨테이너 및 배포 보안

13.1 Docker 보안 모범 사례

# 비루트 사용자로 실행
FROM node:14-slim

# 작업 디렉토리 생성
WORKDIR /app

# 의존성 설치
COPY package*.json ./
RUN npm ci --only=production

# 소스 코드 복사
COPY . .

# 비루트 사용자 생성 및 권한 설정
RUN addgroup --system appgroup && \
    adduser --system --ingroup appgroup appuser && \
    chown -R appuser:appgroup /app

# 비루트 사용자로 전환
USER appuser

# 포트 및 실행 명령
EXPOSE 3000
CMD ["node", "server.js"]

14. 서버 보안 구성

14.1 NODE_ENV 설정

// 프로덕션 환경에서 보안 강화
if (process.env.NODE_ENV === "production") {
  // 디버그 출력 비활성화
  app.set("env", "production");
  app.disable("x-powered-by");

  // strict 모드 활성화
  ("use strict");

  // 오류 세부 정보 숨기기
  app.use((err, req, res, next) => {
    res.status(500).json({ error: "서버 오류" });
    console.error(err);
  });
}

14.2 클린 종료 처리

// 정상 종료 처리
function shutdown() {
  console.log("애플리케이션을 정상 종료합니다...");

  // 열린 연결 종료
  server.close(() => {
    console.log("HTTP 서버 종료됨");

    // 데이터베이스 연결 종료
    mongoose.connection.close(false, () => {
      console.log("MongoDB 연결 종료됨");
      process.exit(0);
    });
  });

  // 강제 종료 타임아웃
  setTimeout(() => {
    console.error("정상 종료에 실패했습니다, 강제 종료합니다");
    process.exit(1);
  }, 10000);
}

// 종료 신호 처리
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);

요약: Node.js 애플리케이션 보안 모범 사례

  1. 인젝션 공격 방지:

    • 매개변수화된 쿼리 사용
    • ORM 활용
    • 입력 데이터 타입 검증
  2. 크로스 사이트 스크립팅(XSS) 방지:

    • 입력 유효성 검사 및 출력 이스케이프
    • Content Security Policy 구현
    • HttpOnly 및 Secure 쿠키 사용
  3. 의존성 관리:

    • 정기적인 의존성 취약점 검사 (npm audit)
    • 최신 패키지 버전 유지
    • CI/CD 파이프라인에 보안 검사 통합
  4. 인증 및 권한 부여:

    • 강력한 비밀번호 정책
    • 보안 해싱 알고리즘 (bcrypt)
    • JWT 보안 강화
    • API 속도 제한
  5. 민감한 데이터 보호:

    • 환경 변수 사용
    • 데이터 암호화
    • HTTPS 사용
  6. 보안 헤더 설정:

    • Helmet 사용
    • 적절한 CORS 설정
  7. 오류 처리 및 로깅:

    • 안전한 오류 처리
    • 구조화된 로깅
    • 민감한 정보 필터링
  8. 입력 유효성 검사:

    • Joi, Express-Validator 등 검증 라이브러리 사용
    • 모든 사용자 입력 검증
  9. CSRF 공격 방지:

    • CSRF 토큰 사용
    • SameSite 쿠키 속성 설정
  10. 보안 모니터링 및 감사:

    • 보안 로깅
    • 중요 작업 감사 추적
    • 모니터링 시스템 구축

Node.js 애플리케이션의 보안은 개발 초기 단계부터 지속적으로 고려되어야 합니다. 위의 모범 사례들을 적용하고 정기적인 보안 감사와 모니터링을 통해 애플리케이션의 보안을 강화할 수 있습니다. 또한 보안은 지속적인 과정이므로 최신 보안 위협과 대응 방법에 대해 항상 업데이트된 정보를 유지하는 것이 중요합니다.

results matching ""

    No results matching ""