Node.js 인터뷰 질문 52

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

답변:

Node.js 애플리케이션은 다양한 보안 취약점에 노출될 수 있습니다. 이러한 취약점을 이해하고 적절한 보안 조치를 취하는 것은 안전한 애플리케이션 개발에 필수적입니다. 아래에서 주요 보안 취약점과 이를 방지하기 위한 모범 사례에 대해 설명하겠습니다.

주요 보안 취약점

1. 인젝션 공격

SQL 인젝션, NoSQL 인젝션, 명령어 인젝션 등의 공격으로, 사용자 입력이 적절히 검증되지 않고 데이터베이스 쿼리나 시스템 명령에 직접 사용될 때 발생합니다.

예시 (SQL 인젝션):

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

방지 방법:

// 안전한 코드 (매개변수화된 쿼리)
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. 크로스 사이트 스크립팅(XSS)

악의적인 스크립트가 웹 페이지에 삽입되어 실행되는 공격입니다.

예시 (취약한 코드):

app.get("/search", (req, res) => {
  const term = req.query.term;
  res.send(`검색 결과: ${term}`); // 사용자 입력이 직접 응답에 삽입됨
});

방지 방법:

const helmet = require("helmet");
const { sanitize } = require("express-sanitizer");
const app = express();

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

// 입력 데이터 살균
app.use(sanitize());

app.get("/search", (req, res) => {
  const term = req.sanitize(req.query.term);
  // 또는 React/Vue 등의 프레임워크 사용 시 자동으로 이스케이프 처리
  res.send(`검색 결과: ${term}`);
});

3. 취약한 의존성

오래된 또는 취약점이 알려진 npm 패키지를 사용하면 애플리케이션이 위험에 노출됩니다.

방지 방법:

# 취약점 검사
npm audit

# 취약한 패키지 수정
npm audit fix

# 의존성 업데이트
npm update

4. 무차별 대입 공격(Brute Force Attacks)

사용자 계정에 대한 반복적인 로그인 시도를 통해 비밀번호를 추측하는 공격입니다.

방지 방법:

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

// 로그인 요청 제한 설정
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 5, // 최대 5번의 요청
  message: "너무 많은 로그인 시도. 15분 후에 다시 시도해주세요.",
});

// 로그인 라우트에 제한 적용
app.post("/login", loginLimiter, (req, res) => {
  // 로그인 처리 로직
});

5. 안전하지 않은 직접 객체 참조(IDOR)

사용자가 권한이 없는 객체에 접근할 수 있는 취약점입니다.

예시 (취약한 코드):

app.get("/api/users/:id/data", (req, res) => {
  const userId = req.params.id;
  // 권한 확인 없이 데이터 반환
  db.getUserData(userId).then((data) => res.json(data));
});

방지 방법:

app.get("/api/users/:id/data", authenticate, (req, res) => {
  const userId = req.params.id;
  const currentUser = req.user;

  // 현재 사용자가 요청된 데이터에 접근할 권한이 있는지 확인
  if (currentUser.id !== userId && !currentUser.isAdmin) {
    return res.status(403).json({ error: "접근 권한 없음" });
  }

  db.getUserData(userId).then((data) => res.json(data));
});

6. 크로스 사이트 요청 위조(CSRF)

사용자가 자신의 의도와 다른 요청을 웹사이트에 전송하도록 속이는 공격입니다.

방지 방법:

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

app.use(cookieParser());
app.use(csrf({ cookie: true }));

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

app.post("/submit", (req, res) => {
  // CSRF 토큰은 자동으로 검증됨
  // 폼 제출 처리
});

7. 잘못된 보안 구성

기본 설정, 불필요한 기능 활성화, 디버그 모드 등 잘못된 보안 구성은 심각한 취약점이 될 수 있습니다.

방지 방법:

// 환경별 구성 설정
const config = {
  development: {
    debugMode: true,
    logLevel: "verbose",
    // 개발용 설정
  },
  production: {
    debugMode: false,
    logLevel: "error",
    // 운영용 보안 설정
  },
};

// 현재 환경에 맞는 설정 사용
const env = process.env.NODE_ENV || "development";
app.use((req, res, next) => {
  req.config = config[env];
  next();
});

보안 모범 사례

1. 입력 유효성 검사 및 살균 처리

사용자 입력은 항상 검증하고 정화해야 합니다.

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

app.post(
  "/signup",
  [
    body("email").isEmail().normalizeEmail(),
    body("password").isLength({ min: 8 }).trim(),
    body("name").not().isEmpty().trim().escape(),
  ],
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    // 유효한 입력으로 처리 계속
  }
);

2. 강력한 인증 및 권한 부여 체계 구현

사용자 인증과 권한 부여 시스템을 강력하게 구현합니다.

// 비밀번호 해싱
const bcrypt = require("bcrypt");

async function registerUser(user) {
  const salt = await bcrypt.genSalt(10);
  user.password = await bcrypt.hash(user.password, salt);

  // 사용자 저장
  return db.saveUser(user);
}

async function loginUser(email, password) {
  const user = await db.getUserByEmail(email);
  if (!user) return null;

  // 비밀번호 검증
  const validPassword = await bcrypt.compare(password, user.password);
  if (!validPassword) return null;

  return user;
}

// JWT 기반 인증
const jwt = require("jsonwebtoken");

function generateAuthToken(user) {
  return jwt.sign({ id: user.id, role: user.role }, process.env.JWT_SECRET, {
    expiresIn: "1h",
  });
}

function authenticate(req, res, next) {
  const token = req.header("x-auth-token");
  if (!token) return res.status(401).json({ error: "인증 토큰이 없습니다." });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (ex) {
    res.status(400).json({ error: "유효하지 않은 토큰입니다." });
  }
}

// 역할 기반 접근 제어(RBAC)
function authorize(roles = []) {
  if (typeof roles === "string") {
    roles = [roles];
  }

  return (req, res, next) => {
    if (!req.user) {
      return res.status(401).json({ error: "인증이 필요합니다." });
    }

    if (roles.length && !roles.includes(req.user.role)) {
      return res.status(403).json({ error: "접근 권한이 없습니다." });
    }

    next();
  };
}

// 사용 예시
app.get("/api/admin", [authenticate, authorize("admin")], (req, res) => {
  // 관리자만 접근 가능한 로직
});

3. HTTPS 사용

모든 프로덕션 환경에서는 HTTPS를 강제로 사용합니다.

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

// HSTS 헤더 설정
app.use(
  helmet.hsts({
    maxAge: 31536000, // 1년
    includeSubDomains: true,
    preload: true,
  })
);

// HTTP를 HTTPS로 리다이렉트
app.use((req, res, next) => {
  if (process.env.NODE_ENV === "production" && !req.secure) {
    return res.redirect(`https://${req.headers.host}${req.url}`);
  }
  next();
});

4. 안전한 HTTP 헤더 설정

Helmet 라이브러리를 사용하여 보안 관련 HTTP 헤더를 설정합니다.

const helmet = require("helmet");
app.use(helmet());

// 특정 헤더 개별 설정
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'", "trusted-cdn.com"],
      // 다른 CSP 설정
    },
  })
);

5. 적절한 에러 처리 및 로깅

에러는 적절히 처리하고 민감한 정보를 노출하지 않도록 합니다.

// 중앙 집중식 에러 처리
app.use((err, req, res, next) => {
  // 에러 로깅
  logger.error(err.stack);

  // 프로덕션에서는 상세 에러 정보 숨기기
  if (process.env.NODE_ENV === "production") {
    return res.status(500).json({ error: "서버 오류가 발생했습니다." });
  }

  // 개발 환경에서는 상세 정보 제공
  res.status(500).json({ error: err.message, stack: err.stack });
});

// 구조화된 로깅
const winston = require("winston");
const logger = winston.createLogger({
  level: process.env.NODE_ENV === "production" ? "info" : "debug",
  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(),
    })
  );
}

6. 보안 의존성 관리

의존성 취약점 검사를 CI/CD 파이프라인에 통합합니다.

// package.json에 보안 스크립트 추가
{
  "scripts": {
    "start": "node index.js",
    "test": "jest",
    "security-check": "npm audit && snyk test"
  }
}

// GitHub Actions workflow 예시
// .github/workflows/security.yml
name: Security Checks

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  schedule:
    - cron: '0 0 * * 0'  # 매주 일요일 실행

jobs:
  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm ci
      - name: Run security audit
        run: npm audit
      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}

7. 민감한 데이터 보호

API 키, 비밀번호 등 민감한 정보는 환경 변수나 안전한 시크릿 관리 서비스를 통해 관리합니다.

// dotenv를 사용한 환경 변수 관리
require("dotenv").config();

// 환경 변수 사용
const db = require("knex")({
  client: "pg",
  connection: {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
  },
});

// AWS Secrets Manager를 사용한 시크릿 관리
const AWS = require("aws-sdk");
const secretsManager = new AWS.SecretsManager();

async function getSecrets() {
  const data = await secretsManager
    .getSecretValue({ SecretId: "my-app-secrets" })
    .promise();
  return JSON.parse(data.SecretString);
}

// 사용 예시
async function initializeApp() {
  const secrets = await getSecrets();
  const stripe = require("stripe")(secrets.STRIPE_API_KEY);
  // 앱 초기화 계속
}

8. 요청 크기 제한 및 속도 제한

DoS 공격을 방지하기 위한 요청 크기 제한과 속도 제한을 구현합니다.

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

// 요청 본문 크기 제한
app.use(express.json({ limit: "10kb" }));
app.use(express.urlencoded({ extended: true, limit: "10kb" }));

// API 요청 속도 제한
const apiLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 100, // IP당 최대 100 요청
  standardHeaders: true, // 표준 RateLimit 헤더 포함
  legacyHeaders: false, // X-RateLimit 헤더 비활성화
  message: "너무 많은 요청. 잠시 후 다시 시도해주세요.",
});

// API 라우트에 제한 적용
app.use("/api/", apiLimiter);

// 특정 라우트에 다른 제한 적용
const createAccountLimiter = rateLimit({
  windowMs: 60 * 60 * 1000, // 1시간
  max: 5, // IP당 최대 5 계정
  message: "너무 많은 계정 생성 시도. 1시간 후에 다시 시도해주세요.",
});

app.post("/create-account", createAccountLimiter, (req, res) => {
  // 계정 생성 로직
});

9. 컨텐츠 보안 정책(CSP) 구현

XSS 공격을 방지하기 위한 CSP를 구현합니다.

const helmet = require("helmet");

// 기본 CSP 설정
app.use(helmet.contentSecurityPolicy());

// 세부 CSP 설정
app.use(
  helmet.contentSecurityPolicy({
    directives: {
      defaultSrc: ["'self'"], // 기본적으로 같은 출처에서만 리소스 로드
      scriptSrc: ["'self'", "https://trusted-cdn.com"], // 스크립트 소스 제한
      styleSrc: ["'self'", "https://fonts.googleapis.com", "'unsafe-inline'"], // 스타일 소스
      imgSrc: ["'self'", "data:", "https://images.example.com"], // 이미지 소스
      connectSrc: ["'self'", "https://api.example.com"], // XMLHttpRequest, WebSocket
      fontSrc: ["'self'", "https://fonts.gstatic.com"], // 폰트 소스
      objectSrc: ["'none'"], // <object>, <embed>, <applet> 태그 차단
      mediaSrc: ["'self'"], // 미디어 소스
      frameSrc: ["'none'"], // <frame>, <iframe> 소스
      // 다른 CSP 옵션들
    },
  })
);

10. 보안 쿠키 설정

쿠키를 통한 세션 하이재킹을 방지하기 위해 보안 쿠키 설정을 구현합니다.

const session = require("express-session");

app.use(
  session({
    secret: process.env.SESSION_SECRET,
    name: "sessionId", // 기본 'connect.sid' 대신 사용자 지정 이름 사용
    cookie: {
      httpOnly: true, // JavaScript에서 쿠키 접근 방지
      secure: process.env.NODE_ENV === "production", // HTTPS에서만 쿠키 전송
      sameSite: "strict", // CSRF 방지
      maxAge: 3600000, // 1시간(ms)
      path: "/", // 쿠키 적용 경로
      domain: process.env.NODE_ENV === "production" ? "example.com" : undefined, // 프로덕션 도메인 제한
    },
    resave: false,
    saveUninitialized: false,
  })
);

11. npm scripts와 package.json의 보안 강화

스크립트 인젝션 공격을 방지하기 위해 보안을 강화합니다.

{
  "scripts": {
    "preinstall": "node security-check.js"
  },
  "engines": {
    "node": ">=14.0.0"
  },
  "engineStrict": true
}

보안 체크 스크립트 예시:

// security-check.js
const fs = require("fs");
const path = require("path");

// package.json 분석
const packageJson = JSON.parse(
  fs.readFileSync(path.join(process.cwd(), "package.json"))
);

// 의심스러운 스크립트 패턴 확인
const suspiciousPatterns = [/curl\s+.*\|.*sh/, /wget\s+.*\|.*sh/, /eval\(/];

Object.values(packageJson.scripts || {}).forEach((script) => {
  if (suspiciousPatterns.some((pattern) => pattern.test(script))) {
    console.error("의심스러운 스크립트 패턴 발견:", script);
    process.exit(1);
  }
});

// 취약한 의존성 확인할 수 있는 로직 추가 가능
// ...

console.log("보안 검사 통과");

12. 보안 헤더 확인 자동화

보안 헤더를 자동으로 확인하기 위한 테스트를 구현합니다.

// test/security-headers.test.js
const request = require("supertest");
const app = require("../app");

describe("Security Headers", () => {
  test("응답은 적절한 보안 헤더를 포함해야 함", async () => {
    const res = await request(app).get("/");

    expect(res.headers["x-content-type-options"]).toBe("nosniff");
    expect(res.headers["x-frame-options"]).toBe("SAMEORIGIN");
    expect(res.headers["x-xss-protection"]).toBe("1; mode=block");
    expect(res.headers["strict-transport-security"]).toBeDefined();
    expect(res.headers["content-security-policy"]).toBeDefined();
  });
});

실제 애플리케이션에서의 종합적 보안 구현

대규모 Node.js 애플리케이션에서는 다양한 보안 모범 사례를 종합적으로 적용해야 합니다.

// app.js
const express = require("express");
const helmet = require("helmet");
const rateLimit = require("express-rate-limit");
const mongoSanitize = require("express-mongo-sanitize");
const xss = require("xss-clean");
const hpp = require("hpp");
const cors = require("cors");
const cookieParser = require("cookie-parser");
const compression = require("compression");
const csrf = require("csurf");

const app = express();

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

// CORS 설정
app.use(
  cors({
    origin:
      process.env.NODE_ENV === "production" ? "https://example.com" : true,
    credentials: true,
  })
);

// 요청 본문 파싱 및 제한
app.use(express.json({ limit: "10kb" }));
app.use(express.urlencoded({ extended: true, limit: "10kb" }));
app.use(cookieParser());

// 데이터 살균
app.use(mongoSanitize()); // NoSQL 쿼리 인젝션 방지
app.use(xss()); // XSS 방지

// HTTP 파라미터 오염 방지
app.use(
  hpp({
    whitelist: ["duration", "sort"], // 중복을 허용할 파라미터
  })
);

// CSRF 보호
app.use(csrf({ cookie: true }));

// 속도 제한
const limiter = rateLimit({
  max: 100, // IP당 최대 요청 수
  windowMs: 60 * 60 * 1000, // 1시간
  message: "너무 많은 요청. 1시간 후에 다시 시도해주세요.",
});
app.use("/api", limiter);

// 압축
app.use(compression());

// 라우트 핸들러
app.use("/api/v1/users", require("./routes/userRoutes"));
app.use("/api/v1/posts", require("./routes/postRoutes"));

// 404 핸들러
app.all("*", (req, res) => {
  res.status(404).json({
    status: "fail",
    message: `${req.originalUrl} 라우트를 찾을 수 없습니다.`,
  });
});

// 전역 에러 핸들러
app.use((err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || "error";

  // 프로덕션 환경에서는 민감한 오류 정보 숨기기
  if (process.env.NODE_ENV === "production") {
    // 운영 에러 응답
    if (err.isOperational) {
      return res.status(err.statusCode).json({
        status: err.status,
        message: err.message,
      });
    }
    // 프로그래밍 또는 알 수 없는 오류
    console.error("ERROR 💥", err);
    return res.status(500).json({
      status: "error",
      message: "서버 오류가 발생했습니다.",
    });
  }

  // 개발 환경에서는 상세 오류 정보 제공
  res.status(err.statusCode).json({
    status: err.status,
    error: err,
    message: err.message,
    stack: err.stack,
  });
});

module.exports = app;

요약

Node.js 애플리케이션의 보안을 강화하기 위해서는 다음과 같은 주요 모범 사례를 준수해야 합니다:

  1. 입력 유효성 검사 및 살균 처리: 모든 사용자 입력 데이터를 적절히 검증하고 정화합니다.
  2. 강력한 인증 및 권한 부여 체계: 안전한 비밀번호 관리, JWT, 역할 기반 접근 제어를 구현합니다.
  3. 보안 HTTP 헤더 설정: Helmet 등의 라이브러리를 사용하여 보안 관련 HTTP 헤더를 설정합니다.
  4. HTTPS 사용: 프로덕션 환경에서는 항상 HTTPS를 사용하고 HSTS를 설정합니다.
  5. 의존성 관리: 정기적으로 취약점을 스캔하고 업데이트합니다.
  6. 속도 제한 및 요청 크기 제한: DoS 공격을 방지하기 위한 제한을 구현합니다.
  7. 에러 처리 및 로깅: 적절한 에러 처리와 로깅으로 민감한 정보 노출을 방지합니다.
  8. 보안 쿠키 설정: 세션 하이재킹을 방지하기 위한 보안 쿠키 설정을 구현합니다.
  9. 컨텐츠 보안 정책(CSP): XSS 공격을 방지하기 위한 CSP를 구현합니다.
  10. 시크릿 관리: 환경 변수나 안전한 시크릿 관리 서비스를 통해 민감한 정보를 관리합니다.

보안은 지속적인 과정입니다. 정기적인 보안 감사, 취약점 테스트, 의존성 업데이트 및 팀 교육을 통해 애플리케이션의 보안 상태를 유지해야 합니다.

results matching ""

    No results matching ""