코드 보안 가이드

웹 애플리케이션의 보안을 한 단계 끌어올리는 실전 코딩 가이드입니다. 복잡한 보안 개념을 이해하기 쉬운 예제와 함께 설명드립니다.

1 입력값 검증 (Input Validation)

모든 사용자 입력은 잠재적인 보안 위협으로 간주해야 합니다. 클라이언트 측 검증만으로는 충분하지 않으며, 서버 측에서 반드시 재검증해야 합니다.

잘못된 예시 ❌

// 위험한 코드 - 직접 데이터베이스 쿼리
const getUserData = async (userId) => {
  const query = `SELECT * FROM users WHERE id = ${userId}`;
  return await db.query(query);
};

올바른 예시 ✅

// 안전한 코드 - 입력값 검증과 매개변수화된 쿼리
const getUserData = async (userId) => {
  // 1. 입력값 타입 검증
  if (!/^\d+$/.test(userId)) {
    throw new Error('Invalid user ID format');
  }

  // 2. 매개변수화된 쿼리 사용
  const query = 'SELECT * FROM users WHERE id = ?';
  return await db.query(query, [userId]);
};

핵심 포인트

  • • 정규표현식을 사용한 형식 검증
  • • 매개변수화된 쿼리로 SQL 인젝션 방지
  • • 입력값 길이와 범위 제한

2 XSS 공격 방지 (Cross-Site Scripting)

XSS 공격은 악성 스크립트를 웹 페이지에 주입하는 공격입니다. 사용자가 입력한 데이터를 그대로 화면에 출력할 때 발생할 수 있습니다.

React에서의 XSS 방지

import DOMPurify from 'dompurify';

// 위험한 방법 - dangerouslySetInnerHTML 직접 사용
const UnsafeComponent = ({ content }) => (
  <div dangerouslySetInnerHTML={{__html: content}} />
);

// 안전한 방법 - DOMPurify로 정화
const SafeComponent = ({ content }) => {
  const sanitizedContent = DOMPurify.sanitize(content);
  return (
    <div dangerouslySetInnerHTML={{__html: sanitizedContent}} />
  );
};

CSP (Content Security Policy) 설정

// Express.js에서 CSP 헤더 설정
const helmet = require('helmet');

app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      'default-src': ["'self'"],
      'script-src': ["'self'", "'unsafe-inline'"],
      'style-src': ["'self'", "'unsafe-inline'"],
      'img-src': ["'self'", 'data:', 'https:'],
    }
  }
}));

실무 팁

  • • React는 기본적으로 XSS 보호 기능을 제공합니다
  • • 사용자 입력을 HTML로 렌더링할 때는 반드시 sanitize 하세요
  • • CSP 헤더로 스크립트 실행을 제한하세요

3 CORS 설정 (Cross-Origin Resource Sharing)

CORS는 다른 도메인에서 리소스에 접근할 때의 보안 정책입니다. 올바른 설정으로 필요한 접근은 허용하고, 악의적인 접근은 차단해야 합니다.

기본 CORS 설정

const cors = require('cors');

// 위험한 설정 - 모든 도메인 허용
app.use(cors({
  origin: '*' // 절대 사용하지 마세요!
}));

// 안전한 설정 - 화이트리스트 기반
const allowedOrigins = [
  'https://www.myapp.com',
  'https://admin.myapp.com',
  'http://localhost:3000' // 개발 환경용
];

app.use(cors({
  origin: function (origin, callback) {
    if (!origin || allowedOrigins.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true, // 쿠키 전송 허용
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

주의사항

  • • 운영 환경에서는 절대 와일드카드(*)를 사용하지 마세요
  • • credentials: true 설정 시 origin을 명시적으로 지정하세요
  • • 필요한 HTTP 메서드만 허용하세요

4 JWT 인증 시스템

JWT(JSON Web Token)를 사용한 인증은 확장성이 좋지만, 적절한 보안 조치 없이는 오히려 취약점이 될 수 있습니다.

안전한 JWT 구현

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// 토큰 생성 시 짧은 만료 시간 설정
const generateTokens = (userId) => {
  const accessToken = jwt.sign(
    { userId, type: 'access' },
    process.env.JWT_SECRET,
    { expiresIn: '15m' } // 15분
  );

  const refreshToken = jwt.sign(
    { userId, type: 'refresh' },
    process.env.REFRESH_SECRET,
    { expiresIn: '7d' } // 7일
  );

  return { accessToken, refreshToken };
};

// 토큰 검증 미들웨어
const verifyToken = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ error: 'Access token required' });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = decoded.userId;
    next();
  } catch (error) {
    return res.status(401).json({ error: 'Invalid token' });
  }
};

보안 모범 사례

  • • 짧은 액세스 토큰 만료 시간 (15분 이하 권장)
  • • 리프레시 토큰으로 자동 갱신 구현
  • • 환경변수로 시크릿 키 관리
  • • 토큰 블랙리스트 기능 구현 고려