SecuritySkill security

该技能涵盖了OWASP安全模式、秘密管理、安全测试等,旨在确保所有项目在合并前通过安全检查,包括输入验证、依赖审计、环境变量管理等关键安全实践。

安全审计 0 次安装 0 次浏览 更新于 3/5/2026

安全技能

使用基础: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验证