$npx -y skills add zhukunpenglinyutong/ai-max --skill security-review在添加认证、处理用户输入、处理密钥、创建 API 端点或实现支付/敏感功能时使用此 skill。提供全面的安全检查清单和模式。
| 1 | # 安全审查 Skill |
| 2 | |
| 3 | 此 skill 确保所有代码遵循安全最佳实践并识别潜在漏洞。 |
| 4 | |
| 5 | ## 何时激活 |
| 6 | |
| 7 | - 实现认证或授权 |
| 8 | - 处理用户输入或文件上传 |
| 9 | - 创建新的 API 端点 |
| 10 | - 处理密钥或凭证 |
| 11 | - 实现支付功能 |
| 12 | - 存储或传输敏感数据 |
| 13 | - 集成第三方 API |
| 14 | |
| 15 | ## 安全检查清单 |
| 16 | |
| 17 | ### 1. 密钥管理 |
| 18 | |
| 19 | #### ❌ 永不这样做 |
| 20 | ```typescript |
| 21 | const apiKey = "sk-proj-xxxxx" // 硬编码的密钥 |
| 22 | const dbPassword = "password123" // 在源代码中 |
| 23 | ``` |
| 24 | |
| 25 | #### ✅ 始终这样做 |
| 26 | ```typescript |
| 27 | const apiKey = process.env.OPENAI_API_KEY |
| 28 | const dbUrl = process.env.DATABASE_URL |
| 29 | |
| 30 | // 验证密钥存在 |
| 31 | if (!apiKey) { |
| 32 | throw new Error('OPENAI_API_KEY not configured') |
| 33 | } |
| 34 | ``` |
| 35 | |
| 36 | #### 验证步骤 |
| 37 | - [ ] 没有硬编码的 API 密钥、令牌或密码 |
| 38 | - [ ] 所有密钥都在环境变量中 |
| 39 | - [ ] `.env.local` 在 .gitignore 中 |
| 40 | - [ ] git 历史中没有密钥 |
| 41 | - [ ] 生产密钥在托管平台(Vercel、Railway)中 |
| 42 | |
| 43 | ### 2. 输入验证 |
| 44 | |
| 45 | #### 始终验证用户输入 |
| 46 | ```typescript |
| 47 | import { z } from 'zod' |
| 48 | |
| 49 | // 定义验证 schema |
| 50 | const CreateUserSchema = z.object({ |
| 51 | email: z.string().email(), |
| 52 | name: z.string().min(1).max(100), |
| 53 | age: z.number().int().min(0).max(150) |
| 54 | }) |
| 55 | |
| 56 | // 处理前验证 |
| 57 | export async function createUser(input: unknown) { |
| 58 | try { |
| 59 | const validated = CreateUserSchema.parse(input) |
| 60 | return await db.users.create(validated) |
| 61 | } catch (error) { |
| 62 | if (error instanceof z.ZodError) { |
| 63 | return { success: false, errors: error.errors } |
| 64 | } |
| 65 | throw error |
| 66 | } |
| 67 | } |
| 68 | ``` |
| 69 | |
| 70 | #### 文件上传验证 |
| 71 | ```typescript |
| 72 | function validateFileUpload(file: File) { |
| 73 | // 大小检查(最大 5MB) |
| 74 | const maxSize = 5 * 1024 * 1024 |
| 75 | if (file.size > maxSize) { |
| 76 | throw new Error('File too large (max 5MB)') |
| 77 | } |
| 78 | |
| 79 | // 类型检查 |
| 80 | const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'] |
| 81 | if (!allowedTypes.includes(file.type)) { |
| 82 | throw new Error('Invalid file type') |
| 83 | } |
| 84 | |
| 85 | // 扩展名检查 |
| 86 | const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif'] |
| 87 | const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0] |
| 88 | if (!extension || !allowedExtensions.includes(extension)) { |
| 89 | throw new Error('Invalid file extension') |
| 90 | } |
| 91 | |
| 92 | return true |
| 93 | } |
| 94 | ``` |
| 95 | |
| 96 | #### 验证步骤 |
| 97 | - [ ] 所有用户输入都使用 schema 验证 |
| 98 | - [ ] 文件上传受限制(大小、类型、扩展名) |
| 99 | - [ ] 没有直接在查询中使用用户输入 |
| 100 | - [ ] 使用白名单验证(而非黑名单) |
| 101 | - [ ] 错误消息不泄露敏感信息 |
| 102 | |
| 103 | ### 3. SQL 注入防护 |
| 104 | |
| 105 | #### ❌ 永不拼接 SQL |
| 106 | ```typescript |
| 107 | // 危险 - SQL 注入漏洞 |
| 108 | const query = `SELECT * FROM users WHERE email = '${userEmail}'` |
| 109 | await db.query(query) |
| 110 | ``` |
| 111 | |
| 112 | #### ✅ 始终使用参数化查询 |
| 113 | ```typescript |
| 114 | // 安全 - 参数化查询 |
| 115 | const { data } = await supabase |
| 116 | .from('users') |
| 117 | .select('*') |
| 118 | .eq('email', userEmail) |
| 119 | |
| 120 | // 或使用原始 SQL |
| 121 | await db.query( |
| 122 | 'SELECT * FROM users WHERE email = $1', |
| 123 | [userEmail] |
| 124 | ) |
| 125 | ``` |
| 126 | |
| 127 | #### 验证步骤 |
| 128 | - [ ] 所有数据库查询使用参数化查询 |
| 129 | - [ ] SQL 中没有字符串拼接 |
| 130 | - [ ] ORM/查询构建器使用正确 |
| 131 | - [ ] Supabase 查询正确清理 |
| 132 | |
| 133 | ### 4. 认证与授权 |
| 134 | |
| 135 | #### JWT 令牌处理 |
| 136 | ```typescript |
| 137 | // ❌ 错误:localStorage(易受 XSS 攻击) |
| 138 | localStorage.setItem('token', token) |
| 139 | |
| 140 | // ✅ 正确:httpOnly cookies |
| 141 | res.setHeader('Set-Cookie', |
| 142 | `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`) |
| 143 | ``` |
| 144 | |
| 145 | #### 授权检查 |
| 146 | ```typescript |
| 147 | export async function deleteUser(userId: string, requesterId: string) { |
| 148 | // 始终先验证授权 |
| 149 | const requester = await db.users.findUnique({ |
| 150 | where: { id: requesterId } |
| 151 | }) |
| 152 | |
| 153 | if (requester.role !== 'admin') { |
| 154 | return NextResponse.json( |
| 155 | { error: 'Unauthorized' }, |
| 156 | { status: 403 } |
| 157 | ) |
| 158 | } |
| 159 | |
| 160 | // 继续执行删除 |
| 161 | await db.users.delete({ where: { id: userId } }) |
| 162 | } |
| 163 | ``` |
| 164 | |
| 165 | #### 行级安全(Supabase) |
| 166 | ```sql |
| 167 | -- 在所有表上启用 RLS |
| 168 | ALTER TABLE users ENABLE ROW LEVEL SECURITY; |
| 169 | |
| 170 | -- 用户只能查看自己的数据 |
| 171 | CREATE POLICY "Users view own data" |
| 172 | ON users FOR SELECT |
| 173 | USING (auth.uid() = id); |
| 174 | |
| 175 | -- 用户只能更新自己的数据 |
| 176 | CREATE POLICY "Users update own data" |
| 177 | ON users FOR UPDATE |
| 178 | USING (auth.uid() = id); |
| 179 | ``` |
| 180 | |
| 181 | #### 验证步骤 |
| 182 | - [ ] 令牌存储在 httpOnly cookies 中(而非 localStorage) |
| 183 | - [ ] 敏感操作前进行授权检查 |
| 184 | - [ ] Supabase 中启用行级安全 |
| 185 | - [ ] 实现基于角色的访问控制 |
| 186 | - [ ] 会话管理安全 |
| 187 | |
| 188 | ### 5. XSS 防护 |
| 189 | |
| 190 | #### 清理 HTML |
| 191 | ```typescript |
| 192 | import DOMPurify from 'isomorphic-dompurify' |
| 193 | |
| 194 | // 始终清理用户提供的 HTML |
| 195 | function renderUserContent(html: string) { |
| 196 | const clean = DOMPurify.sanitize(html, { |
| 197 | ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'], |
| 198 | ALLOWED_ATTR: [] |
| 199 | }) |
| 200 | return <div dangerouslySetInnerHTML={{ __html: clean }} /> |
| 201 | } |
| 202 | ``` |
| 203 | |
| 204 | #### 内容安全策略 |
| 205 | ```typescript |
| 206 | // next.config.js |
| 207 | const securityHeaders = [ |
| 208 | { |
| 209 | key: 'Content-Security-Policy', |
| 210 | value: ` |
| 211 | default-src 'self'; |
| 212 | script-src 'self' 'unsafe-eval' 'unsafe-inline'; |
| 213 | style-src 'self' 'unsafe-inline'; |
| 214 | img-src 'self' data: https:; |
| 215 | font-src 'self'; |
| 216 | connect-src 'self' https://api.example.com; |
| 217 | `.replace(/\s{2,}/g, ' ').trim() |
| 218 | } |
| 219 | ] |
| 220 | ``` |
| 221 | |
| 222 | #### 验证步骤 |
| 223 | - [ ] 用户提供的 HTML 已清理 |
| 224 | - [ ] CSP 头部已配置 |
| 225 | - [ ] 没有未验证的动态内容渲染 |
| 226 | - [ ] 使用 React 内置的 XSS 防护 |
| 227 | |
| 228 | ### 6. CSRF 防护 |
| 229 | |
| 230 | #### CSRF 令牌 |
| 231 | ```typescript |
| 232 | import { csrf } from '@/lib/csrf' |
| 233 | |
| 234 | export async function POST(request: Request) { |
| 235 | const token = request.headers.get('X-CSRF-Token') |
| 236 | |
| 237 | if (!csrf.verify(token)) { |
| 238 | return NextResponse.json( |
| 239 | { error: 'Invalid CSRF token' }, |
| 240 | { status: 403 } |
| 241 | ) |
| 242 | } |
| 243 | |
| 244 | // 处理请求 |
| 245 | } |
| 246 | ``` |
| 247 | |
| 248 | #### SameSite Cookies |
| 249 | ```typescript |
| 250 | res.setHeader('Set-Cookie', |
| 251 | `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`) |
| 252 | ``` |
| 253 | |
| 254 | #### 验证步骤 |
| 255 | - [ ] 状态更改操作使用 CSRF 令牌 |
| 256 | - [ ] 所有 cookies 设置 SameSite=Strict |
| 257 | - [ ] 实现双重提交 cookie 模式 |
| 258 | |
| 259 | ### 7. 速率限制 |
| 260 | |
| 261 | #### API 速率限制 |
| 262 | ```typescript |
| 263 | import rateLimit from 'expr |