Install any skill in seconds. Free to start, no credit card required.
Get Started Free →認証の追加、ユーザー入力の処理、シークレットの操作、APIエンドポイントの作成、支払い/機密機能の実装時にこのスキルを使用します。包括的なセキュリティチェックリストとパターンを提供します。
.claude/skills/loulanyue-security-review/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-05 | ✗→✓ | ▲ Improved | 110% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 157% | 0% |
| case-11 | ✓→✓ | = Same ✓ | 131% | 0% |
| case-01 | ✓→✓ | = Same ✓ | 278% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 179% | 0% |
此技能確保所有程式碼遵循安全性最佳實務並識別潛在漏洞。
typescriptconst apiKey = "sk-proj-xxxxx" // 寫死的密鑰 const dbPassword = "password123" // 在原始碼中
typescriptconst apiKey = process.env.OPENAI_API_KEY const dbUrl = process.env.DATABASE_URL // 驗證密鑰存在 if (!apiKey) { throw new Error('OPENAI_API_KEY not configured') }
.env.local 在 .gitignore 中typescriptimport { z } from 'zod' // 定義驗證 schema const CreateUserSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(100), age: z.number().int().min(0).max(150) }) // 處理前驗證 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 } }
typescriptfunction validateFileUpload(file: File) { // 大小檢查(最大 5MB) const maxSize = 5 * 1024 * 1024 if (file.size > maxSize) { throw new Error('File too large (max 5MB)') } // 類型檢查 const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'] if (!allowedTypes.includes(file.type)) { throw new Error('Invalid file type') } // 副檔名檢查 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 }
typescript// 危險 - SQL 注入漏洞 const query = `SELECT * FROM users WHERE email = '${userEmail}'` await db.query(query)
typescript// 安全 - 參數化查詢 const { data } = await supabase .from('users') .select('*') .eq('email', userEmail) // 或使用原始 SQL await db.query( 'SELECT * FROM users WHERE email = $1', [userEmail] )
typescript// ❌ 錯誤:localStorage(易受 XSS 攻擊) localStorage.setItem('token', token) // ✅ 正確:httpOnly cookies res.setHeader('Set-Cookie', `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
typescriptexport async function deleteUser(userId: string, requesterId: string) { // 總是先驗證授權 const requester = await db.users.findUnique({ where: { id: requesterId } }) if (requester.role !== 'admin') { return NextResponse.json( { error: 'Unauthorized' }, { status: 403 } ) } // 繼續刪除 await db.users.delete({ where: { id: userId } }) }
sql-- 在所有表格上啟用 RLS ALTER TABLE users ENABLE ROW LEVEL SECURITY; -- 使用者只能查看自己的資料 CREATE POLICY "Users view own data" ON users FOR SELECT USING (auth.uid() = id); -- 使用者只能更新自己的資料 CREATE POLICY "Users update own data" ON users FOR UPDATE USING (auth.uid() = id);
typescriptimport DOMPurify from 'isomorphic-dompurify' // 總是淨化使用者提供的 HTML function renderUserContent(html: string) { const clean = DOMPurify.sanitize(html, { ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'], ALLOWED_ATTR: [] }) return <div dangerouslySetInnerHTML={{ __html: clean }} /> }
typescript// next.config.js const securityHeaders = [ { key: 'Content-Security-Policy', value: ` default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' https://api.example.com; `.replace(/\s{2,}/g, ' ').trim() } ]
typescriptimport { csrf } from '@/lib/csrf' export async function POST(request: Request) { const token = request.headers.get('X-CSRF-Token') if (!csrf.verify(token)) { return NextResponse.json( { error: 'Invalid CSRF token' }, { status: 403 } ) } // 處理請求 }
typescriptres.setHeader('Set-Cookie', `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
typescriptimport rateLimit from 'express-rate-limit' const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 分鐘 max: 100, // 每視窗 100 個請求 message: 'Too many requests' }) // 套用到路由 app.use('/api/', limiter)
typescript// 搜尋的積極速率限制 const searchLimiter = rateLimit({ windowMs: 60 * 1000, // 1 分鐘 max: 10, // 每分鐘 10 個請求 message: 'Too many search requests' }) app.use('/api/search', searchLimiter)
typescript// ❌ 錯誤:記錄敏感資料 console.log('User login:', { email, password }) console.log('Payment:', { cardNumber, cvv }) // ✅ 正確:遮蔽敏感資料 console.log('User login:', { email, userId }) console.log('Payment:', { last4: card.last4, userId })
typescript// ❌ 錯誤:暴露內部細節 catch (error) { return NextResponse.json( { error: error.message, stack: error.stack }, { status: 500 } ) } // ✅ 正確:通用錯誤訊息 catch (error) { console.error('Internal error:', error) return NextResponse.json( { error: 'An error occurred. Please try again.' }, { status: 500 } ) }
typescriptimport { verify } from '@solana/web3.js' async function verifyWalletOwnership( publicKey: string, signature: string, message: string ) { try { const isValid = verify( Buffer.from(message), Buffer.from(signature, 'base64'), Buffer.from(publicKey, 'base64') ) return isValid } catch (error) { return false } }
typescriptasync function verifyTransaction(transaction: Transaction) { // 驗證收款人 if (transaction.to !== expectedRecipient) { throw new Error('Invalid recipient') } // 驗證金額 if (transaction.amount > maxAmount) { throw new Error('Amount exceeds limit') } // 驗證使用者有足夠餘額 const balance = await getBalance(transaction.from) if (balance < transaction.amount) { throw new Error('Insufficient balance') } return true }
bash# 檢查漏洞 npm audit # 自動修復可修復的問題 npm audit fix # 更新依賴 npm update # 檢查過時套件 npm outdated
bash# 總是 commit lock 檔案 git add package-lock.json # 在 CI/CD 中使用以獲得可重現的建置 npm ci # 而非 npm install
typescript// 測試認證 test('requires authentication', async () => { const response = await fetch('/api/protected') expect(response.status).toBe(401) }) // 測試授權 test('requires admin role', async () => { const response = await fetch('/api/admin', { headers: { Authorization: `Bearer ${userToken}` } }) expect(response.status).toBe(403) }) // 測試輸入驗證 test('rejects invalid input', async () => { const response = await fetch('/api/users', { method: 'POST', body: JSON.stringify({ email: 'not-an-email' }) }) expect(response.status).toBe(400) }) // 測試速率限制 test('enforces rate limits', async () => { const requests = Array(101).fill(null).map(() => fetch('/api/endpoint') ) const responses = await Promise.all(requests) const tooManyRequests = responses.filter(r => r.status === 429) expect(tooManyRequests.length).toBeGreaterThan(0) })
任何生產部署前:
記住:安全性不是可選的。一個漏洞可能危及整個平台。有疑慮時,選擇謹慎的做法。
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-11 | pass→pass | 11,231 | 5,456 | -51% | 1 | 1 | 0% | 2,006 | 4,627 | +131% | 0 | 0 | — |
case-01 | pass→pass | 7,292 | 7,584 | +4% | 1 | 1 | 0% | 1,325 | 5,009 | +278% | 0 | 0 | — |
case-02 | fail→fail | 12,540 | 13,243 | +6% | 1 | 1 | 0% | 2,436 | 6,206 | +155% | 0 | 0 | — |
case-03 | pass→pass | 10,424 | 8,874 | -15% | 1 | 1 | 0% | 1,863 | 5,200 | +179% | 0 | 0 | — |
case-04 | fail→fail | 17,780 | 16,087 | -10% | 1 | 1 | 0% | 3,419 | 6,648 | +94% | 0 | 0 | — |
case-05 | fail→pass | 15,779 | 10,220 | -35% | 1 | 1 | 0% | 2,594 | 5,455 | +110% | 0 | 0 | — |
case-06 | pass→pass | 8,915 | 8,086 | -9% | 1 | 1 | 0% | 1,646 | 5,074 | +208% | 0 | 0 | — |
case-07 | pass→pass | 12,222 | 11,767 | -4% | 1 | 1 | 0% | 2,107 | 5,764 | +174% | 0 | 0 | — |
case-08 | pass→pass | 16,088 | 12,361 | -23% | 1 | 1 | 0% | 2,981 | 5,850 | +96% | 0 | 0 | — |
case-09 | fail→fail | 13,058 | 7,578 | -42% | 1 | 1 | 0% | 2,519 | 4,995 | +98% | 0 | 0 | — |
case-10 | pass→pass | 11,343 | 7,911 | -30% | 1 | 1 | 0% | 1,984 | 5,069 | +155% | 0 | 0 | — |
case-12 | fail→fail | 13,076 | 11,373 | -13% | 1 | 1 | 0% | 2,321 | 5,771 | +149% | 0 | 0 | — |
case-13 | fail→fail | 18,443 | 16,239 | -12% | 1 | 1 | 0% | 3,489 | 6,692 | +92% | 0 | 0 | — |
case-14 | pass→pass | 7,698 | 5,563 | -28% | 1 | 1 | 0% | 1,305 | 4,534 | +247% | 0 | 0 | — |
case-15 | pass→pass | 11,085 | 10,054 | -9% | 1 | 1 | 0% | 2,073 | 5,672 | +174% | 0 | 0 | — |
case-16 | pass→pass | 14,271 | 15,626 | +9% | 1 | 1 | 0% | 2,898 | 6,652 | +130% | 0 | 0 | — |
case-17 | fail→fail | 13,825 | 10,494 | -24% | 1 | 1 | 0% | 2,632 | 5,502 | +109% | 0 | 0 | — |
case-18 | fail→pass | 15,371 | 14,603 | -5% | 1 | 1 | 0% | 2,477 | 6,361 | +157% | 0 | 0 | — |
case-19 | pass→pass | 8,385 | 6,137 | -27% | 1 | 1 | 0% | 1,524 | 4,678 | +207% | 0 | 0 | — |
case-20 | pass→pass | 20,816 | 17,000 | -18% | 1 | 1 | 0% | 3,569 | 6,559 | +84% | 0 | 0 | — |
case-21 | pass→pass | 18,209 | 18,169 | -0% | 1 | 1 | 0% | 3,321 | 7,018 | +111% | 0 | 0 | — |
case-22 | pass→pass | 10,258 | 9,286 | -9% | 1 | 1 | 0% | 2,059 | 5,417 | +163% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +9 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.