Initial commit: Anvil Lounge chat application
- React frontend with Vite + TypeScript - Cloudflare Worker backend with Durable Objects - AI-powered chat moderation via OpenAI - WebSocket-based real-time messaging - XSS prevention, rate limiting, input validation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
225
docs/BUG_FIXES_2026-01.md
Normal file
225
docs/BUG_FIXES_2026-01.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Chat App 버그 수정 보고서
|
||||
|
||||
**수정일**: 2026-01-19
|
||||
**검토 범위**: frontend/, worker/
|
||||
|
||||
---
|
||||
|
||||
## 요약
|
||||
|
||||
| 우선순위 | 발견 | 수정 |
|
||||
|---------|------|------|
|
||||
| Critical | 4 | 4 |
|
||||
| High | 4 | 4 |
|
||||
| Medium | 9 | 9 |
|
||||
| **합계** | **17** | **17** |
|
||||
|
||||
---
|
||||
|
||||
## Critical Issues
|
||||
|
||||
### 1. WebSocket Race Condition
|
||||
**파일**: `frontend/src/hooks/useWebSocket.ts`
|
||||
**증상**: 빠른 연속 호출 시 중복 WebSocket 연결 생성
|
||||
**원인**: 연결 상태 확인 전에 lock 획득이 이루어지지 않음
|
||||
**수정**:
|
||||
```typescript
|
||||
const connect = useCallback(() => {
|
||||
// CRITICAL: Atomic lock acquisition to prevent race conditions
|
||||
if (isConnectingRef.current) {
|
||||
return;
|
||||
}
|
||||
// Acquire lock FIRST, before any other checks
|
||||
isConnectingRef.current = true;
|
||||
// ...
|
||||
```
|
||||
|
||||
### 2. RateLimiter Memory Leak
|
||||
**파일**: `worker/src/RateLimiter.ts`
|
||||
**증상**: 장시간 운영 시 메모리 사용량 증가
|
||||
**원인**: `cleanup()` 메서드가 만료된 엔트리를 제대로 삭제하지 않음
|
||||
**수정**:
|
||||
- blocked 상태 만료 시 즉시 삭제
|
||||
- 빈 timestamp 배열 엔트리 삭제
|
||||
- timestamp 배열 정리 로직 개선
|
||||
|
||||
### 3. API Key Exposure in Logs
|
||||
**파일**: `worker/src/AIManager.ts`
|
||||
**증상**: 에러 로그에 API 키가 노출될 가능성
|
||||
**원인**: `sanitizeErrorForLogging()` 함수의 패턴 누락
|
||||
**수정**: 다양한 API 키 형식 지원
|
||||
- OpenAI: `sk-proj-*`, `sk-svcacct-*`
|
||||
- Anthropic: `sk-ant-api03-*`
|
||||
- AWS: `AKIA*`, `ASIA*`
|
||||
- Cloudflare tokens
|
||||
- Basic auth credentials
|
||||
|
||||
### 4. Silent Storage Failure
|
||||
**파일**: `worker/src/ChatRoom.ts`
|
||||
**증상**: DO storage 실패 시 메시지 유실
|
||||
**원인**: `saveMessage()`에 retry 로직 부재
|
||||
**수정**:
|
||||
```typescript
|
||||
const STORAGE_MAX_RETRIES = 3;
|
||||
const STORAGE_RETRY_BASE_DELAY_MS = 50;
|
||||
|
||||
private async saveMessage(message: StoredMessage): Promise<void> {
|
||||
for (let attempt = 1; attempt <= STORAGE_MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
// storage operation
|
||||
return;
|
||||
} catch (error) {
|
||||
if (attempt < STORAGE_MAX_RETRIES) {
|
||||
const jitter = Math.random() * STORAGE_RETRY_BASE_DELAY_MS;
|
||||
await delay(STORAGE_RETRY_BASE_DELAY_MS * attempt + jitter);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## High Priority Issues
|
||||
|
||||
### 5. XSS via URL Encoding
|
||||
**파일**: `frontend/src/utils/sanitize.ts`
|
||||
**증상**: `javascript:` 같은 인코딩된 XSS 공격 가능
|
||||
**원인**: URL 검증 시 인코딩된 문자열 미처리
|
||||
**수정**: `isUrlSafe()` 함수 추가
|
||||
```typescript
|
||||
function isUrlSafe(url: string): boolean {
|
||||
const decoded = decodeURIComponent(url);
|
||||
const doubleDecoded = decodeURIComponent(decoded);
|
||||
// Check all versions for dangerous protocols
|
||||
const dangerousProtocols = [
|
||||
/^javascript:/, /^vbscript:/, /^data:/,
|
||||
/^file:/, /^about:/, /^blob:/,
|
||||
];
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 6. TOCTOU Race Condition (Silence/Violations)
|
||||
**파일**: `worker/src/ChatRoom.ts`
|
||||
**증상**: 동시 요청 시 silence 체크 우회 가능
|
||||
**원인**: read-check-write가 atomic하지 않음
|
||||
**수정**: DO transaction 사용
|
||||
```typescript
|
||||
private async isUserSilenced(userName: string): Promise<boolean> {
|
||||
return await this.ctx.storage.transaction(async (txn) => {
|
||||
const entry = await txn.get<SilenceEntry>(storageKey);
|
||||
if (!entry) return false;
|
||||
if (Date.now() >= entry.expiresAt) {
|
||||
await txn.delete(storageKey);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Unvalidated JSON Parsing
|
||||
**파일**: `worker/src/AIManager.ts`
|
||||
**증상**: OpenAI API 응답 형식 변경 시 런타임 에러
|
||||
**원인**: 응답 JSON 검증 부재
|
||||
**수정**: Zod schema 추가
|
||||
```typescript
|
||||
const OpenAIResponseSchema = z.object({
|
||||
choices: z.array(z.object({
|
||||
message: z.object({
|
||||
content: z.string().nullable(),
|
||||
tool_calls: z.array(OpenAIToolCallSchema).optional(),
|
||||
}),
|
||||
finish_reason: z.string(),
|
||||
})),
|
||||
});
|
||||
```
|
||||
|
||||
### 8. Missing API Timeout
|
||||
**파일**: `worker/src/Context7Client.ts`
|
||||
**증상**: 외부 API 무응답 시 무한 대기
|
||||
**원인**: fetch 요청에 timeout 미설정
|
||||
**수정**: `fetchWithTimeout()` 함수 추가 (10초 timeout)
|
||||
|
||||
---
|
||||
|
||||
## Medium Priority Issues
|
||||
|
||||
### 9. Frontend Message Array Memory Leak
|
||||
**파일**: `frontend/src/components/ChatRoom.tsx`, `frontend/src/config.ts`
|
||||
**증상**: 장시간 채팅 시 브라우저 메모리 증가
|
||||
**원인**: 메시지 배열 무한 증가
|
||||
**수정**:
|
||||
```typescript
|
||||
// config.ts
|
||||
export const CHAT_CONFIG = {
|
||||
maxDisplayMessages: 200,
|
||||
} as const;
|
||||
|
||||
// ChatRoom.tsx
|
||||
function limitMessages(messages: DisplayMessage[]): DisplayMessage[] {
|
||||
if (messages.length > CHAT_CONFIG.maxDisplayMessages) {
|
||||
return messages.slice(-CHAT_CONFIG.maxDisplayMessages);
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
```
|
||||
|
||||
### 10. Config Mismatch
|
||||
**파일**: `frontend/src/config.ts`
|
||||
**증상**: 프론트엔드에서 2000자 메시지 전송 시 잘림
|
||||
**원인**: 프론트엔드(1000) vs 백엔드(2000) 제한 불일치
|
||||
**수정**: `maxMessageLength: 2000`으로 통일
|
||||
|
||||
### 11. Context7 Cache Unbounded Growth
|
||||
**파일**: `worker/src/Context7Client.ts`
|
||||
**증상**: 캐시 크기 무한 증가 가능
|
||||
**원인**: 캐시 cleanup 로직 부재
|
||||
**수정**:
|
||||
- `MAX_CACHE_SIZE = 100` 제한
|
||||
- `cleanupExpired()`: 만료 엔트리 정리
|
||||
- `evictOldest()`: LRU 방식 eviction
|
||||
- `cleanupIfNeeded()`: 1분 간격 자동 정리
|
||||
|
||||
### 12. Serialization Error Handling
|
||||
**파일**: `worker/src/ChatRoom.ts`
|
||||
**증상**: WebSocket attachment 직렬화 실패 시 silent failure
|
||||
**원인**: `serializeAttachment()` 에러 처리 부재
|
||||
**수정**:
|
||||
```typescript
|
||||
private setSession(ws: WebSocket, session: Session): boolean {
|
||||
try {
|
||||
ws.serializeAttachment(session);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to serialize session attachment:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 수정된 파일 목록
|
||||
|
||||
| 파일 | 수정 내용 |
|
||||
|------|----------|
|
||||
| `frontend/src/hooks/useWebSocket.ts` | Race condition 수정 |
|
||||
| `frontend/src/utils/sanitize.ts` | XSS URL 검증 강화 |
|
||||
| `frontend/src/components/ChatRoom.tsx` | 메시지 배열 제한 |
|
||||
| `frontend/src/config.ts` | 설정값 통일 및 추가 |
|
||||
| `worker/src/ChatRoom.ts` | Storage retry, TOCTOU 수정, serialization 에러 처리 |
|
||||
| `worker/src/AIManager.ts` | API key redaction, Zod validation |
|
||||
| `worker/src/RateLimiter.ts` | Memory leak 수정 |
|
||||
| `worker/src/Context7Client.ts` | Timeout 추가, cache cleanup |
|
||||
|
||||
---
|
||||
|
||||
## 테스트 권장사항
|
||||
|
||||
1. **WebSocket 연결 테스트**: 빠른 연속 연결/해제 시 중복 연결 없음 확인
|
||||
2. **XSS 테스트**: `javascript:alert(1)` 같은 인코딩된 URL 차단 확인
|
||||
3. **메모리 테스트**: 장시간 채팅 후 브라우저 메모리 사용량 확인
|
||||
4. **Storage 테스트**: DO storage 일시적 실패 시 retry 동작 확인
|
||||
5. **동시성 테스트**: 동일 사용자 동시 요청 시 silence 체크 정상 동작 확인
|
||||
Reference in New Issue
Block a user