# 安全性實作指南 ## 🔒 安全性總覽 DramaLing 遵循 OWASP 安全標準,實施多層防護策略。 ## 🛡️ 認證與授權 ### 1. NextAuth 配置 ```typescript // lib/auth.ts import { NextAuthOptions } from 'next-auth' import CredentialsProvider from 'next-auth/providers/credentials' import GoogleProvider from 'next-auth/providers/google' import { createClient } from '@supabase/supabase-js' import bcrypt from 'bcryptjs' export const authOptions: NextAuthOptions = { providers: [ CredentialsProvider({ name: 'credentials', credentials: { email: { label: "Email", type: "email" }, password: { label: "Password", type: "password" } }, async authorize(credentials) { if (!credentials?.email || !credentials?.password) { throw new Error('Missing credentials') } const supabase = createClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SUPABASE_SERVICE_ROLE_KEY! ) const { data: user } = await supabase .from('users') .select('*') .eq('email', credentials.email) .single() if (!user || !await bcrypt.compare(credentials.password, user.password)) { throw new Error('Invalid credentials') } return { id: user.id, email: user.email, name: user.name, } } }), GoogleProvider({ clientId: process.env.GOOGLE_CLIENT_ID!, clientSecret: process.env.GOOGLE_CLIENT_SECRET!, }) ], session: { strategy: 'jwt', maxAge: 30 * 24 * 60 * 60, // 30 days }, jwt: { secret: process.env.NEXTAUTH_SECRET, }, callbacks: { async jwt({ token, user }) { if (user) { token.userId = user.id } return token }, async session({ session, token }) { if (session.user) { session.user.id = token.userId as string } return session }, }, } ``` ### 2. 密碼安全 ```typescript // utils/password.ts import bcrypt from 'bcryptjs' import { z } from 'zod' // 密碼驗證規則 export const passwordSchema = z.string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Password must contain at least one uppercase letter') .regex(/[a-z]/, 'Password must contain at least one lowercase letter') .regex(/[0-9]/, 'Password must contain at least one number') .regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character') // 密碼雜湊 export async function hashPassword(password: string): Promise { return bcrypt.hash(password, 12) } // 密碼驗證 export async function verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash) } ``` ## 🔐 API 安全 ### 1. Rate Limiting ```typescript // middleware/rateLimit.ts import { Ratelimit } from '@upstash/ratelimit' import { Redis } from '@upstash/redis' const redis = new Redis({ url: process.env.UPSTASH_REDIS_REST_URL!, token: process.env.UPSTASH_REDIS_REST_TOKEN!, }) const ratelimit = new Ratelimit({ redis, limiter: Ratelimit.slidingWindow(10, '10 s'), // 10 requests per 10 seconds }) export async function rateLimitMiddleware(req: Request) { const ip = req.headers.get('x-forwarded-for') ?? 'anonymous' const { success, limit, reset, remaining } = await ratelimit.limit(ip) if (!success) { return new Response('Too Many Requests', { status: 429, headers: { 'X-RateLimit-Limit': limit.toString(), 'X-RateLimit-Remaining': remaining.toString(), 'X-RateLimit-Reset': new Date(reset).toISOString(), }, }) } return null } ``` ### 2. CORS 配置 ```typescript // next.config.js module.exports = { async headers() { return [ { source: '/api/:path*', headers: [ { key: 'Access-Control-Allow-Credentials', value: 'true' }, { key: 'Access-Control-Allow-Origin', value: process.env.ALLOWED_ORIGIN || '*' }, { key: 'Access-Control-Allow-Methods', value: 'GET,DELETE,PATCH,POST,PUT' }, { key: 'Access-Control-Allow-Headers', value: 'Authorization, Content-Type' }, ], }, ] }, } ``` ### 3. API 路由保護 ```typescript // app/api/flashcards/route.ts import { getServerSession } from 'next-auth' import { authOptions } from '@/lib/auth' import { rateLimitMiddleware } from '@/middleware/rateLimit' export async function POST(req: Request) { // Rate limiting const rateLimitResponse = await rateLimitMiddleware(req) if (rateLimitResponse) return rateLimitResponse // 認證檢查 const session = await getServerSession(authOptions) if (!session) { return new Response('Unauthorized', { status: 401 }) } // 輸入驗證 const body = await req.json() const validation = flashcardSchema.safeParse(body) if (!validation.success) { return new Response(JSON.stringify({ errors: validation.error.errors }), { status: 400, headers: { 'Content-Type': 'application/json' }, }) } // 處理請求... } ``` ## 🧹 輸入驗證與消毒 ### 1. XSS 防護 ```typescript // utils/sanitize.ts import DOMPurify from 'isomorphic-dompurify' export function sanitizeHTML(html: string): string { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a', 'p', 'br'], ALLOWED_ATTR: ['href', 'target'], }) } // 使用範例 export function FlashcardContent({ content }: { content: string }) { return (
) } ``` ### 2. SQL Injection 防護 ```typescript // 使用參數化查詢(Supabase/Prisma 自動處理) // ❌ 錯誤:直接字串串接 const query = `SELECT * FROM users WHERE email = '${email}'` // ✅ 正確:使用參數化查詢 const { data } = await supabase .from('users') .select('*') .eq('email', email) // 自動參數化 ``` ### 3. 檔案上傳安全 ```typescript // utils/fileUpload.ts import { z } from 'zod' const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'image/webp'] const MAX_FILE_SIZE = 5 * 1024 * 1024 // 5MB export const fileSchema = z.object({ name: z.string(), size: z.number().max(MAX_FILE_SIZE, 'File too large'), type: z.enum(ALLOWED_FILE_TYPES as [string, ...string[]]), }) export async function validateFile(file: File) { // 檢查檔案類型 if (!ALLOWED_FILE_TYPES.includes(file.type)) { throw new Error('Invalid file type') } // 檢查檔案大小 if (file.size > MAX_FILE_SIZE) { throw new Error('File too large') } // 檢查檔案內容(Magic Number) const buffer = await file.arrayBuffer() const bytes = new Uint8Array(buffer) const signatures = { jpeg: [0xFF, 0xD8, 0xFF], png: [0x89, 0x50, 0x4E, 0x47], } // 驗證檔案簽名... return true } ``` ## 🔑 環境變數安全 ### 1. 環境變數分離 ```bash # .env.local (開發環境) NEXT_PUBLIC_APP_URL=http://localhost:3000 # .env.production (生產環境) NEXT_PUBLIC_APP_URL=https://dramaling.com # .env.vault (加密儲存敏感資料) SUPABASE_SERVICE_ROLE_KEY=encrypted:xxx ``` ### 2. Secrets 管理 ```typescript // utils/secrets.ts export function getSecret(key: string): string { const value = process.env[key] if (!value) { throw new Error(`Missing required environment variable: ${key}`) } // 生產環境檢查 if (process.env.NODE_ENV === 'production') { if (value.includes('test') || value.includes('example')) { throw new Error(`Invalid production value for ${key}`) } } return value } ``` ## 🛑 錯誤處理安全 ### 1. 安全的錯誤訊息 ```typescript // utils/errorHandler.ts export function sanitizeError(error: unknown): { message: string; code: string } { // 生產環境:隱藏詳細錯誤 if (process.env.NODE_ENV === 'production') { console.error('Internal error:', error) // 記錄到伺服器日誌 return { message: 'An error occurred. Please try again later.', code: 'INTERNAL_ERROR', } } // 開發環境:顯示詳細錯誤 if (error instanceof Error) { return { message: error.message, code: 'DEV_ERROR', } } return { message: 'Unknown error', code: 'UNKNOWN', } } ``` ## 🔍 安全標頭 ```typescript // middleware.ts export function middleware(request: NextRequest) { const response = NextResponse.next() // Security headers response.headers.set('X-Frame-Options', 'DENY') response.headers.set('X-Content-Type-Options', 'nosniff') response.headers.set('X-XSS-Protection', '1; mode=block') response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin') response.headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()') // Content Security Policy response.headers.set( 'Content-Security-Policy', "default-src 'self'; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://apis.google.com; " + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " + "font-src 'self' https://fonts.gstatic.com; " + "img-src 'self' data: https: blob:; " + "connect-src 'self' https://*.supabase.co https://generativelanguage.googleapis.com;" ) return response } ``` ## 📊 安全監控 ### 1. 審計日誌 ```typescript // utils/audit.ts interface AuditLog { userId: string action: string resource: string timestamp: Date ip?: string userAgent?: string } export async function logAuditEvent(event: AuditLog) { await supabase.from('audit_logs').insert({ ...event, timestamp: new Date().toISOString(), }) } // 使用範例 await logAuditEvent({ userId: session.user.id, action: 'DELETE_FLASHCARD', resource: `flashcard:${flashcardId}`, ip: request.headers.get('x-forwarded-for'), userAgent: request.headers.get('user-agent'), }) ``` ### 2. 異常檢測 ```typescript // utils/security/anomaly.ts export async function detectAnomalies(userId: string) { // 檢查異常登入模式 const recentLogins = await getRecentLogins(userId) // 檢查異常 API 使用 const apiUsage = await getAPIUsage(userId) if (apiUsage.count > 1000) { await flagAccount(userId, 'EXCESSIVE_API_USAGE') } // 檢查異常數據存取 const dataAccess = await getDataAccessPatterns(userId) if (dataAccess.uniqueIPs > 5) { await flagAccount(userId, 'MULTIPLE_IP_ACCESS') } } ``` ## ✅ 安全檢查清單 ### 開發階段 - [ ] 所有 API 路由都有認證檢查 - [ ] 所有用戶輸入都經過驗證 - [ ] 敏感數據都已加密 - [ ] 實施 Rate Limiting - [ ] 設置安全標頭 - [ ] 錯誤訊息不洩露敏感資訊 ### 部署前 - [ ] 移除所有 console.log - [ ] 更新所有依賴到最新安全版本 - [ ] 執行安全掃描(npm audit) - [ ] 設置 HTTPS - [ ] 配置防火牆規則 - [ ] 啟用監控和警報 ### 定期檢查 - [ ] 每週檢查安全日誌 - [ ] 每月更新依賴 - [ ] 每季進行安全審計 - [ ] 定期備份數據 - [ ] 測試災難恢復流程 ## 🚨 事件響應計劃 ### 安全事件處理流程 1. **檢測** - 監控系統發現異常 2. **評估** - 確定影響範圍 3. **隔離** - 限制受影響系統 4. **修復** - 修補漏洞 5. **恢復** - 恢復正常運作 6. **檢討** - 事後分析改進 ### 緊急聯絡 - 安全團隊:security@dramaling.com - 24/7 監控:monitor@dramaling.com - 法律顧問:legal@dramaling.com