安全技能
使用基础:base.md
所有项目的安全最佳实践和自动化安全测试。
核心原则
安全不是可选的。 每个项目在合并前必须通过安全检查。假设所有输入都是恶意的,所有秘密如果提交将会泄露,所有依赖都有漏洞。
必需的安全设置
1. Gitignore(不可协商)
每个项目必须在.gitignore中有以下内容:
# 环境文件 - 永不提交
.env
.env.*
!.env.example
# 秘密
*.pem
*.key
*.p12
*.pfx
credentials.json
secrets.json
*-credentials.json
service-account*.json
# IDE和操作系统
.idea/
.vscode/settings.json
.DS_Store
Thumbs.db
# 依赖
node_modules/
__pycache__/
*.pyc
.venv/
venv/
# 构建输出
dist/
build/
*.egg-info/
# 可能包含敏感数据的日志
*.log
logs/
2. 环境变量
创建.env.example 包含所有必需的变量(无值):
# .env.example - 复制到.env并填写值
# 仅服务器端(永不以VITE_或NEXT_PUBLIC_为前缀)
DATABASE_URL=
ANTHROPIC_API_KEY=
SUPABASE_SERVICE_ROLE_KEY=
# 客户端安全(公共,非敏感)
VITE_SUPABASE_URL=
VITE_SUPABASE_ANON_KEY=
前端环境变量(关键!)
永远不要在客户端暴露的环境变量中放置秘密:
| 框架 | 客户端暴露前缀 | 仅限服务器 |
|---|---|---|
| Vite | VITE_* |
无前缀 |
| Next.js | NEXT_PUBLIC_* |
无前缀 |
| Create React App | REACT_APP_* |
N/A(无服务器) |
// 错误 - 秘密暴露给浏览器捆绑包!
const apiKey = import.meta.env.VITE_ANTHROPIC_API_KEY;
// 正确 - 仅客户端公共值
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
// 正确 - 秘密仅保留在服务器端
// 在API路由或服务器函数中:
const apiKey = process.env.ANTHROPIC_API_KEY;
Vercel环境变量:
- 在Vercel仪表板中,没有
VITE_前缀的秘密是仅限服务器的 - 只有
VITE_*变量被捆绑到客户端代码中 - 总是在浏览器开发工具→源代码→你的捆绑包中验证秘密没有暴露
在启动时验证环境:
// config/env.ts
import { z } from 'zod';
const envSchema = z.object({
DATABASE_URL: z.string().url(),
ANTHROPIC_API_KEY: z.string().min(1),
NODE_ENV: z.enum(['development', 'production', 'test']),
});
export const env = envSchema.parse(process.env);
# config/env.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
anthropic_api_key: str
environment: str = "development"
class Config:
env_file = ".env"
settings = Settings()
安全测试
提交前安全检查
添加到提交前钩子:
对于所有项目:
# .pre-commit-config.yaml(添加到现有)
repos:
# 检测秘密
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
args: ['--baseline', '.secrets.baseline']
# 检查依赖中的安全问题
- repo: local
hooks:
- id: security-check
name: security-check
entry: ./scripts/security-check.sh
language: script
pass_filenames: false
TypeScript/JavaScript:
// package.json脚本
{
"scripts": {
"security:audit": "npm audit --audit-level=high",
"security:secrets": "npx secretlint '**/*'",
"security:deps": "npx better-npm-audit audit"
}
}
Python:
# 添加到开发依赖
pip install safety bandit
# 命令
safety check # 检查依赖中的漏洞
bandit -r src/ # 静态安全分析
安全检查脚本
创建scripts/security-check.sh:
#!/bin/bash
set -e
echo "Running security checks..."
# 检查暂存文件中的秘密
echo "Checking for secrets..."
if command -v detect-secrets &> /dev/null; then
detect-secrets scan --baseline .secrets.baseline
fi
# 检查.env没有暂存
if git diff --cached --name-only | grep -E '^\.env$|^\.env\.' | grep -v '\.example$'; then
echo "ERROR: .env file is staged for commit!"
exit 1
fi
# 检查暂存文件中的常见秘密模式
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
if echo "$STAGED_FILES" | xargs grep -l -E '(password|secret|api_key|apikey|token|private_key)\s*[:=]\s*["\047][^"\047]+["\047]' 2>/dev/null; then
echo "ERROR: Possible secrets found in staged files!"
exit 1
fi
# 语言特定的检查
if [ -f "package.json" ]; then
echo "Checking npm dependencies..."
npm audit --audit-level=high || echo "Warning: npm audit found issues"
fi
if [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then
echo "Checking Python dependencies..."
if command -v safety &> /dev/null; then
safety check || echo "Warning: safety found issues"
fi
fi
echo "Security checks passed!"
chmod +x scripts/security-check.sh
GitHub Actions安全工作流
创建.github/workflows/security.yml:
name: Security
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
# 每周一上午9点UTC运行
- cron: '0 9 * * 1'
jobs:
secrets-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Detect secrets
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.pull_request.base.sha }}
head: ${{ github.event.pull_request.head.sha }}
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Node.js项目
- name: Setup Node
if: hashFiles('package.json') != ''
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
if: hashFiles('package.json') != ''
run: npm ci
- name: NPM Audit
if: hashFiles('package.json') != ''
run: npm audit --audit-level=high
# Python项目
- name: Setup Python
if: hashFiles('pyproject.toml') != '' || hashFiles('requirements.txt') != ''
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install safety
if: hashFiles('pyproject.toml') != '' || hashFiles('requirements.txt') != ''
run: pip install safety
- name: Safety check
if: hashFiles('pyproject.toml') != '' || hashFiles('requirements.txt') != ''
run: safety check
codeql:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ hashFiles('package.json') != '' && 'javascript-typescript' || 'python' }}
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
输入验证(OWASP Top 10)
1. SQL注入防护
永远不要使用字符串拼接:
// BAD - SQL注入易受攻击
const user = await db.query(`SELECT * FROM users WHERE id = ${userId}`);
// GOOD - 参数化查询
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);
// GOOD - 使用ORM(Kysely, Prisma, Drizzle)
const user = await db.selectFrom('users').where('id', '=', userId).execute();
# BAD - SQL注入易受攻击
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# GOOD - 参数化查询
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
# GOOD - 使用ORM(SQLAlchemy)
user = session.query(User).filter(User.id == user_id).first()
2. XSS防护
// 始终在渲染前清理用户输入
import DOMPurify from 'dompurify';
// BAD - XSS易受攻击
element.innerHTML = userInput;
// GOOD - 已清理
element.innerHTML = DOMPurify.sanitize(userInput);
// BEST - 使用框架内置的转义(React默认这样做)
return <div>{userInput}</div>; // 在React中安全
// DANGER - 绕过React的保护
return <div dangerouslySetInnerHTML={{ __html: userInput }} />; // 避免!
3. 输入验证在边界
// 使用Zod验证所有外部输入
import { z } from 'zod';
const CreateUserSchema = z.object({
email: z.string().email().max(255),
name: z.string().min(1).max(100).regex(/^[a-zA-Z\s]+$/),
age: z.number().int().min(0).max(150),
});
// 在路由处理程序中
app.post('/users', async (req, res) => {
const result = CreateUserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({ error: result.error });
}
// result.data现在是已类型化和验证的
});
4. 路径遍历防护
import path from 'path';
// BAD - 路径遍历易受攻击
const filePath = `./uploads/${req.params.filename}`;
// GOOD - 验证和清理路径
const filename = path.basename(req.params.filename); // Strips ../
const filePath = path.join('./uploads', filename);
// 验证它仍在允许的目录内
if (!filePath.startsWith(path.resolve('./uploads'))) {
throw new Error('Invalid path');
}
认证与授权
JWT最佳实践
import jwt from 'jsonwebtoken';
// 令牌生成
function generateToken(userId: string): string {
return jwt.sign(
{ sub: userId },
process.env.JWT_SECRET!,
{
expiresIn: '15m', // 短期访问令牌
algorithm: 'HS256',
}
);
}
// 令牌验证
function verifyToken(token: string): { sub: string } {
return jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'], // 明确指定允许的算法
}) as { sub: string };
}
密码哈希
import bcrypt from 'bcrypt';
const SALT_ROUNDS = 12; // 最小10,推荐12+
async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, SALT_ROUNDS);
}
async function verifyPassword(password: string, hash: string): Promise<boolean> {
return bcrypt.compare(password, hash);
}
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
def hash_password(password: str) -> str:
return pwd_context.hash(password)
def verify_password(password: str, hashed: str) -> bool:
return pwd_context.verify(password, hashed)
限流
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 每个窗口100个请求
standardHeaders: true,
legacyHeaders: false,
});
// 应用于认证路由
app.use('/api/auth', rateLimit({
windowMs: 60 * 1000, // 1分钟
max: 5, // 每分钟5次尝试
message: 'Too many login attempts, please try again later',
}));
安全头部
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
},
}));
安全测试清单
在每次发布前运行:
# 安全清单
### 秘密与环境
- [ ] 代码中没有秘密(运行detect-secrets)
- [ ] .env文件在.gitignore中
- [ ] .env.example存在,包含所有必需的变量
- [ ] 环境在启动时已验证
### 依赖
- [ ] npm审计/安全检查通过
- [ ] 依赖中没有已知漏洞
- [ ] 依赖是最新的(Dependabot启用)
### 输入验证
- [ ] 所有API输入都使用模式验证(Zod/Pydantic)
- [ ] 文件上传限制类型和大小
- [ ] 防止路径遍历
### 认证
- [ ] 使用bcrypt哈希密码(12+轮)
- [ ] JWT使用短期过期
- [ ] 认证端点限流
- [ ] 登录时会话令牌轮换
### 数据库
- [ ] 仅使用参数化查询
- [ ] 最小权限数据库用户
- [ ] 连接字符串未记录
### 头部与CORS
- [ ] 启用安全头部(helmet)
- [ ] CORS限制已知来源
- [ ] 生产中仅HTTPS
### 日志记录
- [ ] 日志中没有秘密
- [ ] 日志中没有PII(或适当掩蔽)
- [ ] 记录失败的认证尝试
安全反模式
- ❌
VITE_*,NEXT_PUBLIC_*, 或REACT_APP_*环境变量中的秘密(客户端暴露!) - ❌ 代码或配置文件中的秘密提交到git
- ❌ 没有.gitignore条目的.env文件
- ❌ 用于SQL查询的字符串拼接
- ❌ 未经清理的
dangerouslySetInnerHTML - ❌ 使用用户输入的
eval()或new Function() - ❌ 以明文或弱哈希(MD5, SHA1)存储密码
- ❌ 无过期或过期时间很长的JWT
- ❌ 认证端点没有限流
- ❌ 日志记录敏感数据(密码,令牌,PII)
- ❌ 生产中CORS来源使用
* - ❌ 忽略npm审计/安全检查警告
- ❌ 生产中以root/admin运行
- ❌ 任何环境的硬编码凭据
- ❌ 禁用SSL/TLS验证