XSS 공격 방지: Express.js 및 React 앱 보안 전략

XSS 공격 이해 및 Express와 React 애플리케이션에서의 예방

카테고리

인프라/DevOps/보안

서브카테고리

보안

대상자

  • *대상자**: Express.js와 React를 사용하는 프론트엔드/백엔드 개발자
  • *난이도**: 중급 이상 (보안 개념과 웹 개발 기술 이해 필요)

핵심 요약

  • 입력 검증 강화: express-validator를 사용해 클라이언트/서버 측에서 데이터를 검증 및 정화 (escape() 메서드 활용)
  • CSP 헤더 구현: helmet 라이브러리로 Content-Security-Policy 설정 (예: defaultSrc: ["'self'"])
  • HTML 렌더링 시 보안: React의 dangerouslySetInnerHTML 사용 제한 및 DOMPurify로 HTML 내용 정화

섹션별 세부 요약

1. XSS 공격 유형

  • Reflected XSS: URL 파라미터나 폼 입력을 통해 서버에서 즉시 반영된 스크립트 실행
  • Stored XSS: 데이터베이스, 댓글 필드 등에 영구 저장된 악성 스크립트
  • DOM-based XSS: 클라이언트 측 JavaScript가 DOM을 비안전하게 조작하여 발생

2. Express.js 애플리케이션의 공격 벡터

  • 비검증된 쿼리 파라미터 반영
  • 정화 없이 저장된 사용자 생성 콘텐츠
  • 템플릿 엔진을 통한 동적 HTML 생성
  • 검증 없이 사용자 입력을 반환하는 API 엔드포인트

3. React 애플리케이션의 고려사항

  • dangerouslySetInnerHTML 사용 시 반드시 DOMPurify로 정화
  • 사용자 입력 기반의 동적 스크립트 로딩
  • 제3자 컴포넌트의 취약점
  • 클라이언트 측 라우팅 시 파라미터 정화

4. 입력 검증 구현 예제

const { body, validationResult } = require('express-validator');
app.post('/user/comment', [
  body('comment')
    .trim()
    .isLength({ min: 1, max: 500 })
    .escape()
    .withMessage('1-500자 사이로 입력하세요')
], (req, res) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
});

5. CSP 헤더 구성

const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", "data:", "https:"]
  }
}));

6. 템플릿 엔진과 React의 정화

  • EJS: <%- %>는 신뢰 콘텐츠, <%= %>는 자동 정화
  • React JSX: 자동 정화 지원, dangerouslySetInnerHTML 사용 시 DOMPurify로 필터링

7. 동적 라우팅 파라미터 검증

import { useParams } from 'react-router-dom';
const UserProfile = () => {
  const { userId } = useParams();
  if (!/^\d+$/.test(userId)) return 
잘못된 사용자 ID
; };

8. 외부 API 응답 정화

const sanitizeApiResponse = (data) => {
  if (typeof data === 'string') return DOMPurify.sanitize(data);
  if (typeof data === 'object') {
    const sanitized = {};
    for (const [key, value] of Object.entries(data)) {
      sanitized[key] = sanitizeApiResponse(value);
    }
    return sanitized;
  }
  return data;
};

9. 보안 헤더 추가

app.use((req, res, next) => {
  res.setHeader('X-Content-Type-Options', 'nosniff');
  res.setHeader('X-Frame-Options', 'DENY');
  res.setHeader('X-XSS-Protection', '1; mode=block');
  res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
  next();
});

10. 보안 테스트 통합 예제

describe('XSS Prevention', () => {
  test('사용자 입력 정화 테스트', async () => {
    const maliciousInput = '';
    const response = await request(app).post('/api/comment').send({ comment: maliciousInput });
    expect(response.body.comment).not.toContain('