IT Knowledge/Security/images/api-보안-diagram.svg

📌 핵심 개념

쉬운 비유: API는 식당의 주문 창구. 안전한 API는 “신분증 확인 → 메뉴만 주문 가능 → 주문 횟수 제한” 같은 규칙이 있는 창구입니다.

🎯 필수 보안 조치

1. API 키 인증

쉬운 비유: 식당 멤버십 카드. 카드가 있어야 주문할 수 있고, 카드마다 사용 내역이 기록됩니다.

// ❌ 나쁜 예: API 키 없음
app.get('/api/users', (req, res) => {
  // 누구나 호출 가능 → 악용 위험
  const users = db.users.findAll();
  res.json(users);
});
 
// ✅ 좋은 예: API 키 필수
const API_KEYS = new Map([
  ['key_abc123', { client: 'Mobile App', limit: 1000 }],
  ['key_xyz789', { client: 'Web App', limit: 5000 }]
]);
 
function validateApiKey(req, res, next) {
  const apiKey = req.header('X-API-Key');
 
  if (!apiKey) {
    return res.status(401).json({ error: 'API 키가 필요합니다' });
  }
 
  const keyInfo = API_KEYS.get(apiKey);
  if (!keyInfo) {
    return res.status(403).json({ error: '유효하지 않은 API 키' });
  }
 
  req.apiKeyInfo = keyInfo;
  next();
}
 
app.get('/api/users', validateApiKey, (req, res) => {
  // API 키가 유효한 경우에만 실행
  const users = db.users.findAll();
  res.json(users);
});

2. Rate Limiting (속도 제한)

쉬운 비유: 뷔페 식당의 “1인당 3회 방문 제한”. 한 사람이 무한정 음식을 가져가면 다른 손님이 피해를 봅니다.

const rateLimit = require('express-rate-limit');
 
// IP 기반 속도 제한
const generalLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15분
  max: 100,  // 최대 100 요청
  message: '요청이 너무 많습니다. 15분 후 다시 시도하세요.',
  standardHeaders: true,  // RateLimit-* 헤더 반환
  legacyHeaders: false
});
 
// 로그인 시도 제한 (더 엄격)
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,  // 최대 5회
  skipSuccessfulRequests: true  // 성공하면 카운트 안 함
});
 
// 사용
app.use('/api/', generalLimiter);
app.post('/api/login', loginLimiter, loginHandler);
 
// API 키별 속도 제한
const keyUsage = new Map();
 
function rateLimitByApiKey(req, res, next) {
  const apiKey = req.header('X-API-Key');
  const now = Date.now();
 
  if (!keyUsage.has(apiKey)) {
    keyUsage.set(apiKey, { count: 0, resetTime: now + 60000 });
  }
 
  const usage = keyUsage.get(apiKey);
 
  // 1분 경과 시 리셋
  if (now > usage.resetTime) {
    usage.count = 0;
    usage.resetTime = now + 60000;
  }
 
  // 제한 확인
  const limit = req.apiKeyInfo.limit;
  if (usage.count >= limit) {
    return res.status(429).json({
      error: '요청 한도 초과',
      limit,
      resetIn: Math.ceil((usage.resetTime - now) / 1000)
    });
  }
 
  usage.count++;
  next();
}

3. CORS (Cross-Origin Resource Sharing)

쉬운 비유: 식당이 “우리 가게 단골 고객만 테이크아웃 가능”이라고 정한 것. 다른 가게 손님은 주문 못 함.

const cors = require('cors');
 
// ❌ 나쁜 예: 모든 출처 허용
app.use(cors({
  origin: '*'  // 어떤 웹사이트에서든 API 호출 가능 → 위험
}));
 
// ✅ 좋은 예: 허용된 출처만
const allowedOrigins = [
  'https://myapp.com',
  'https://www.myapp.com',
  'https://mobile.myapp.com'
];
 
app.use(cors({
  origin: (origin, callback) => {
    // origin이 없으면 (모바일 앱 등) 허용
    if (!origin) return callback(null, true);
 
    if (allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('CORS 정책 위반'));
    }
  },
  credentials: true,  // 쿠키 허용
  maxAge: 86400  // Preflight 캐시 (24시간)
}));
 
// 특정 라우트만 CORS 허용
app.get('/api/public', cors(), (req, res) => {
  // 이 엔드포인트는 모두에게 공개
  res.json({ message: 'Public data' });
});

4. 입력 검증

쉬운 비유: 식당에서 “라면 10개”는 정상 주문이지만 “라면 100만 개”는 비정상. 주문을 검증해야 합니다.

const { body, param, validationResult } = require('express-validator');
 
// 검증 규칙 정의
app.post('/api/users',
  [
    body('email')
      .isEmail()
      .withMessage('올바른 이메일 형식이 아닙니다')
      .normalizeEmail(),
 
    body('age')
      .isInt({ min: 1, max: 150 })
      .withMessage('나이는 1-150 사이여야 합니다'),
 
    body('username')
      .isLength({ min: 3, max: 20 })
      .withMessage('사용자명은 3-20자여야 합니다')
      .matches(/^[a-zA-Z0-9_]+$/)
      .withMessage('사용자명은 영문, 숫자, _만 가능합니다')
  ],
  (req, res) => {
    // 검증 결과 확인
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
 
    // 검증 통과 후 처리
    const user = db.users.insert(req.body);
    res.json(user);
  }
);
 
// 숫자형 파라미터 검증
app.get('/api/posts/:id',
  param('id').isInt().withMessage('ID는 숫자여야 합니다'),
  (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }
 
    const post = db.posts.findById(req.params.id);
    res.json(post);
  }
);

5. HTTPS 강제

쉬운 비유: 편지를 투명 봉투가 아닌 불투명 봉투에 넣어 보내기. 중간에 누가 내용을 못 봅니다.

// HTTP → HTTPS 리다이렉트
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https' && process.env.NODE_ENV === 'production') {
    return res.redirect(`https://${req.header('host')}${req.url}`);
  }
  next();
});
 
// HSTS 헤더 (브라우저에게 HTTPS만 사용하도록 강제)
app.use((req, res, next) => {
  res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
  next();
});

6. 민감 정보 숨기기

쉬운 비유: 식당 주방을 손님에게 공개하지 않기. 레시피나 조리 과정은 비밀입니다.

// ❌ 나쁜 예: 에러 메시지로 시스템 정보 노출
app.use((err, req, res, next) => {
  res.status(500).json({
    error: err.message,
    stack: err.stack,  // 스택 트레이스 노출!
    sql: err.sql  // SQL 쿼리 노출!
  });
});
 
// ✅ 좋은 예: 일반적인 에러 메시지
app.use((err, req, res, next) => {
  // 서버 로그에만 상세 정보 기록
  console.error('Error:', err);
 
  // 클라이언트에는 간단한 메시지만
  res.status(500).json({
    error: '서버 오류가 발생했습니다'
  });
});
 
// X-Powered-By 헤더 제거
app.disable('x-powered-by');
 
// 민감한 필드 제거
app.get('/api/users/:id', async (req, res) => {
  const user = await db.users.findById(req.params.id);
 
  // 비밀번호, 내부 ID 등 제거
  const { password, internalId, ssn, ...publicUser } = user;
 
  res.json(publicUser);
});

📊 보안 수준별 API 설계

Level 1: 공개 API

// 누구나 사용 가능, 속도 제한만
app.get('/api/public/products', generalLimiter, (req, res) => {
  const products = db.products.findAll();
  res.json(products);
});

Level 2: API 키 필요

// API 키 + 속도 제한
app.get('/api/products', validateApiKey, rateLimitByApiKey, (req, res) => {
  const products = db.products.findAll();
  res.json(products);
});

Level 3: 사용자 인증 필요

// JWT 토큰 + 권한 확인
app.post('/api/products', authenticateToken, authorize('admin'), (req, res) => {
  const product = db.products.insert(req.body);
  res.json(product);
});

💡 보안 체크리스트

필수 (모든 API)

  • HTTPS 사용
  • Rate Limiting
  • 입력 검증
  • CORS 설정
  • 에러 메시지 통제

권장 (중요 API)

  • API 키 또는 JWT 인증
  • 로깅 및 모니터링
  • API 버전 관리
  • 문서화 (Swagger/OpenAPI)

고급 (민감 API)

  • IP 화이트리스트
  • 암호화된 요청/응답
  • 감사 로그
  • 침입 탐지 시스템

🔗 참고 자료