--- name: security-review description: 인증 추가, 사용자 입력 처리, 시크릿 관리, API 엔드포인트 생성, 결제/민감한 기능 구현 시 이 스킬을 사용하세요. 포괄적인 보안 체크리스트와 패턴을 제공합니다. origin: ECC --- # 보안 리뷰 스킬 이 스킬은 모든 코드가 보안 모범 사례를 따르고 잠재적 취약점을 식별하도록 보장합니다. ## 활성화 시점 - 인증 또는 권한 부여 구현 시 - 사용자 입력 또는 파일 업로드 처리 시 - 새로운 API 엔드포인트 생성 시 - 시크릿 또는 자격 증명 작업 시 - 결제 기능 구현 시 - 민감한 데이터 저장 또는 전송 시 - 서드파티 API 통합 시 ## 보안 체크리스트 ### 1. 시크릿 관리 #### 절대 하지 말아야 할 것 ```typescript const apiKey = "sk-proj-xxxxx" // Hardcoded secret const dbPassword = "password123" // In source code ``` #### 반드시 해야 할 것 ```typescript const apiKey = process.env.OPENAI_API_KEY const dbUrl = process.env.DATABASE_URL // Verify secrets exist if (!apiKey) { throw new Error('OPENAI_API_KEY not configured') } ``` #### 확인 단계 - [ ] 하드코딩된 API 키, 토큰, 비밀번호 없음 - [ ] 모든 시크릿이 환경 변수에 저장됨 - [ ] `.env.local`이 .gitignore에 포함됨 - [ ] git 히스토리에 시크릿 없음 - [ ] 프로덕션 시크릿이 호스팅 플랫폼(Vercel, Railway)에 저장됨 ### 2. 입력 유효성 검사 #### 항상 사용자 입력을 검증할 것 ```typescript import { z } from 'zod' // Define validation schema const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(100), age: z.number().int().min(0).max(150) }) // Validate before processing export async function createUser(input: unknown) { try { const validated = CreateUserSchema.parse(input) return await db.users.create(validated) } catch (error) { if (error instanceof z.ZodError) { return { success: false, errors: error.errors } } throw error } } ``` #### 파일 업로드 유효성 검사 ```typescript function validateFileUpload(file: File) { // Size check (5MB max) const maxSize = 5 * 1024 * 1024 if (file.size > maxSize) { throw new Error('File too large (max 5MB)') } // Type check const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'] if (!allowedTypes.includes(file.type)) { throw new Error('Invalid file type') } // Extension check const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'] const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0] if (!extension || !allowedExtensions.includes(extension)) { throw new Error('Invalid file extension') } return true } ``` #### 확인 단계 - [ ] 모든 사용자 입력이 스키마로 검증됨 - [ ] 파일 업로드가 제한됨 (크기, 타입, 확장자) - [ ] 사용자 입력이 쿼리에 직접 사용되지 않음 - [ ] 화이트리스트 검증 사용 (블랙리스트가 아닌) - [ ] 에러 메시지가 민감한 정보를 노출하지 않음 ### 3. SQL Injection 방지 #### 절대 SQL을 연결하지 말 것 ```typescript // DANGEROUS - SQL Injection vulnerability const query = `SELECT * FROM users WHERE email = '${userEmail}'` await db.query(query) ``` #### 반드시 파라미터화된 쿼리를 사용할 것 ```typescript // Safe - parameterized query const { data } = await supabase .from('users') .select('*') .eq('email', userEmail) // Or with raw SQL await db.query( 'SELECT * FROM users WHERE email = $1', [userEmail] ) ``` #### 확인 단계 - [ ] 모든 데이터베이스 쿼리가 파라미터화된 쿼리 사용 - [ ] SQL에서 문자열 연결 없음 - [ ] ORM/쿼리 빌더가 올바르게 사용됨 - [ ] Supabase 쿼리가 적절히 새니타이징됨 ### 4. 인증 및 권한 부여 #### JWT 토큰 처리 ```typescript // FAIL: WRONG: localStorage (vulnerable to XSS) localStorage.setItem('token', token) // PASS: CORRECT: httpOnly cookies res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`) ``` #### 권한 부여 확인 ```typescript export async function deleteUser(userId: string, requesterId: string) { // ALWAYS verify authorization first const requester = await db.users.findUnique({ where: { id: requesterId } }) if (requester.role !== 'admin') { return NextResponse.json( { error: 'Unauthorized' }, { status: 403 } ) } // Proceed with deletion await db.users.delete({ where: { id: userId } }) } ``` #### Row Level Security (Supabase) ```sql -- Enable RLS on all tables ALTER TABLE users ENABLE ROW LEVEL SECURITY; -- Users can only view their own data CREATE POLICY "Users view own data" ON users FOR SELECT USING (auth.uid() = id); -- Users can only update their own data CREATE POLICY "Users update own data" ON users FOR UPDATE USING (auth.uid() = id); ``` #### 확인 단계 - [ ] 토큰이 httpOnly 쿠키에 저장됨 (localStorage가 아닌) - [ ] 민감한 작업 전에 권한 부여 확인 - [ ] Supabase에서 Row Level Security 활성화됨 - [ ] 역할 기반 접근 제어 구현됨 - [ ] 세션 관리가 안전함 ### 5. XSS 방지 #### HTML 새니타이징 ```typescript import DOMPurify from 'isomorphic-dompurify' // ALWAYS sanitize user-provided HTML function renderUserContent(html: string) { const clean = DOMPurify.sanitize(html, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'], ALLOWED_ATTR: [] }) return
} ``` #### Content Security Policy ```typescript // next.config.js const securityHeaders = [ { key: 'Content-Security-Policy', value: ` default-src 'self'; script-src 'self' 'nonce-{nonce}'; style-src 'self' 'nonce-{nonce}'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; `.replace(/\s{2,}/g, ' ').trim() } ] ``` `{nonce}`는 요청마다 새로 생성하고, 헤더와 인라인 `