# Security And Hardening

> 加固代码以防止漏洞。适用于处理用户输入、认证、数据存储或外部集成时，也适用于构建任何接收不可信数据、管理用户会话，或与第三方服务交互的功能。

- Skill: `233i/security-and-hardening` (Agent Skill)
- Install (CLI): `npx skillmds@latest add 233i/security-and-hardening`
- Raw SKILL.md: https://api.skillmd.com/api/skills/233i/security-and-hardening/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: 233i (https://skillmd.com/u/233i)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/233i/security-and-hardening

---


# 安全与加固

## 概览

以安全为优先的 Web 应用开发实践。把每一个外部输入都视为潜在恶意，把每一个 secret 都视为不可泄露，把每一次授权检查都视为必须执行。安全不是某个单独阶段，而是所有涉及用户数据、认证和外部系统代码的共同约束。

## 何时使用

- 构建任何接收用户输入的功能
- 实现认证或授权
- 存储或传输敏感数据
- 集成外部 API 或服务
- 新增文件上传、webhook 或 callback
- 处理支付或 PII 数据

## 三层边界系统

### 始终要做（Always Do）

- **校验所有外部输入**，在系统边界处完成，例如 API route、表单处理器
- **所有数据库查询都参数化**，绝不把用户输入拼进 SQL
- **输出做编码**，防止 XSS，优先依赖框架自动转义，不要绕过
- **所有外部通信都使用 HTTPS**
- **密码必须哈希存储**，使用 bcrypt / scrypt / argon2，绝不能明文
- **设置安全头**，例如 CSP、HSTS、X-Frame-Options、X-Content-Type-Options
- **会话 cookie 使用 `httpOnly`、`secure`、`sameSite`**
- **每次发布前运行 `npm audit`** 或等价工具

### 先询问（Ask First）

- 新增认证流程或修改现有 auth 逻辑
- 存储新的敏感数据类别，例如 PII 或支付信息
- 新增外部服务集成
- 修改 CORS 配置
- 增加文件上传处理器
- 调整限流或节流策略
- 授予更高权限或角色

### 绝不要做（Never Do）

- **绝不要把 secrets 提交进版本控制**，例如 API key、密码、token
- **绝不要记录敏感数据**，例如密码、token、完整信用卡号
- **绝不要把前端校验当作安全边界**
- **绝不要为了方便关闭安全头**
- **绝不要在用户提供的数据上使用 `eval()` 或 `innerHTML`**
- **绝不要把会话存进客户端可读存储**，例如 localStorage 中保存 auth token
- **绝不要把堆栈或内部错误细节直接暴露给用户**

## OWASP Top 10 预防

### 1. 注入（SQL、NoSQL、OS Command）

```typescript
// BAD: SQL injection via string concatenation
const query = `SELECT * FROM users WHERE id = '${userId}'`;

// GOOD: Parameterized query
const user = await db.query('SELECT * FROM users WHERE id = $1', [userId]);

// GOOD: ORM with parameterized input
const user = await prisma.user.findUnique({ where: { id: userId } });
```

### 2. 认证失效

```typescript
// Password hashing
import { hash, compare } from 'bcrypt';

const SALT_ROUNDS = 12;
const hashedPassword = await hash(plaintext, SALT_ROUNDS);
const isValid = await compare(plaintext, hashedPassword);

// Session management
app.use(session({
  secret: process.env.SESSION_SECRET,  // From environment, not code
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,     // Not accessible via JavaScript
    secure: true,       // HTTPS only
    sameSite: 'lax',    // CSRF protection
    maxAge: 24 * 60 * 60 * 1000,  // 24 hours
  },
}));
```

### 3. 跨站脚本（XSS）

```typescript
// BAD: Rendering user input as HTML
element.innerHTML = userInput;

// GOOD: Use framework auto-escaping (React does this by default)
return <div>{userInput}</div>;

// If you MUST render HTML, sanitize first
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(userInput);
```

### 4. 访问控制失效

```typescript
// Always check authorization, not just authentication
app.patch('/api/tasks/:id', authenticate, async (req, res) => {
  const task = await taskService.findById(req.params.id);

  // Check that the authenticated user owns this resource
  if (task.ownerId !== req.user.id) {
    return res.status(403).json({
      error: { code: 'FORBIDDEN', message: 'Not authorized to modify this task' }
    });
  }

  // Proceed with update
  const updated = await taskService.update(req.params.id, req.body);
  return res.json(updated);
});
```

### 5. 安全配置错误

```typescript
// Security headers (use helmet for Express)
import helmet from 'helmet';
app.use(helmet());

// Content Security Policy
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],
    styleSrc: ["'self'", "'unsafe-inline'"],  // Tighten if possible
    imgSrc: ["'self'", 'data:', 'https:'],
    connectSrc: ["'self'"],
  },
}));

// CORS — restrict to known origins
app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(',') || 'http://localhost:3000',
  credentials: true,
}));
```

### 6. 敏感数据暴露

```typescript
// Never return sensitive fields in API responses
function sanitizeUser(user: UserRecord): PublicUser {
  const { passwordHash, resetToken, ...publicFields } = user;
  return publicFields;
}

// Use environment variables for secrets
const API_KEY = process.env.STRIPE_API_KEY;
if (!API_KEY) throw new Error('STRIPE_API_KEY not configured');
```

## 输入校验模式

### 在边界做 Schema 校验

```typescript
import { z } from 'zod';

const CreateTaskSchema = z.object({
  title: z.string().min(1).max(200).trim(),
  description: z.string().max(2000).optional(),
  priority: z.enum(['low', 'medium', 'high']).default('medium'),
  dueDate: z.string().datetime().optional(),
});

// Validate at the route handler
app.post('/api/tasks', async (req, res) => {
  const result = CreateTaskSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: {
        code: 'VALIDATION_ERROR',
        message: 'Invalid input',
        details: result.error.flatten(),
      },
    });
  }
  // result.data is now typed and validated
  const task = await taskService.create(result.data);
  return res.status(201).json(task);
});
```

### 文件上传安全

```typescript
// Restrict file types and sizes
const ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/webp'];
const MAX_SIZE = 5 * 1024 * 1024; // 5MB

function validateUpload(file: UploadedFile) {
  if (!ALLOWED_TYPES.includes(file.mimetype)) {
    throw new ValidationError('File type not allowed');
  }
  if (file.size > MAX_SIZE) {
    throw new ValidationError('File too large (max 5MB)');
  }
  // Don't trust the file extension — check magic bytes if critical
}
```

## 如何分诊 `npm audit` 结果

不是所有 audit 结果都需要立即处理，用下面这棵树判断：

```
npm audit reports a vulnerability
├── Severity: critical or high
│   ├── Is the vulnerable code reachable in your app?
│   │   ├── YES --> Fix immediately (update, patch, or replace the dependency)
│   │   └── NO (dev-only dep, unused code path) --> Fix soon, but not a blocker
│   └── Is a fix available?
│       ├── YES --> Update to the patched version
│       └── NO --> Check for workarounds, consider replacing the dependency, or add to allowlist with a review date
├── Severity: moderate
│   ├── Reachable in production? --> Fix in the next release cycle
│   └── Dev-only? --> Fix when convenient, track in backlog
└── Severity: low
    └── Track and fix during regular dependency updates
```

**关键问题：**
- 漏洞函数真的会被你的运行路径调用到吗？
- 这是 runtime 依赖，还是纯开发依赖？
- 在你的部署上下文里它真的可利用吗？例如一个只影响服务端的漏洞，对纯前端应用可能并不成立

如果决定延期处理，必须记录理由并设置复查日期。

## 限流

```typescript
import rateLimit from 'express-rate-limit';

// General API rate limit
app.use('/api/', rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100,                   // 100 requests per window
  standardHeaders: true,
  legacyHeaders: false,
}));

// Stricter limit for auth endpoints
app.use('/api/auth/', rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 10,  // 10 attempts per 15 minutes
}));
```

## Secrets 管理

```
.env files:
  ├── .env.example  → Committed (template with placeholder values)
  ├── .env          → NOT committed (contains real secrets)
  └── .env.local    → NOT committed (local overrides)

.gitignore must include:
  .env
  .env.local
  .env.*.local
  *.pem
  *.key
```

**每次提交前都检查：**
```bash
# Check for accidentally staged secrets
git diff --cached | grep -i "password\|secret\|api_key\|token"
```

## 安全评审清单

```markdown
### Authentication
- [ ] Passwords hashed with bcrypt/scrypt/argon2 (salt rounds ≥ 12)
- [ ] Session tokens are httpOnly, secure, sameSite
- [ ] Login has rate limiting
- [ ] Password reset tokens expire

### Authorization
- [ ] Every endpoint checks user permissions
- [ ] Users can only access their own resources
- [ ] Admin actions require admin role verification

### Input
- [ ] All user input validated at the boundary
- [ ] SQL queries are parameterized
- [ ] HTML output is encoded/escaped

### Data
- [ ] No secrets in code or version control
- [ ] Sensitive fields excluded from API responses
- [ ] PII encrypted at rest (if applicable)

### Infrastructure
- [ ] Security headers configured (CSP, HSTS, etc.)
- [ ] CORS restricted to known origins
- [ ] Dependencies audited for vulnerabilities
- [ ] Error messages don't expose internals
```

## 常见自我安慰

| 自我安慰 | 现实 |
|---|---|
| “这是内部工具，安全没那么重要” | 内部工具一样会被打。攻击者永远找最弱的一环。 |
| “安全以后再补” | 安全补丁式后加，难度通常是从一开始就做好它的 10 倍。 |
| “没人会来利用这个洞” | 自动化扫描器会发现它。靠隐蔽性不是安全。 |
| “框架会帮我们兜底安全问题” | 框架提供的是工具，不是保证。你仍然要正确使用它。 |
| “这只是原型” | 原型最后常常会进生产。安全习惯必须第一天就建立。 |

## 危险信号

- 用户输入被直接传进数据库查询、shell 命令或 HTML 渲染
- 源码或提交历史里出现 secrets
- API endpoint 没有认证或授权检查
- CORS 没配置，或 origin 直接写成 `*`
- 认证接口没有限流
- 把堆栈或内部错误暴露给用户
- 依赖中存在已知 critical 漏洞

## 验证

完成任何与安全相关的实现后，确认：

- [ ] `npm audit` 没有 critical 或 high 漏洞
- [ ] 源码和 git 历史里没有 secrets
- [ ] 所有用户输入都在系统边界被校验
- [ ] 每个受保护 endpoint 都做了认证和授权检查
- [ ] 响应里带有安全头，可用浏览器 DevTools 检查
- [ ] 错误响应不暴露内部细节
- [ ] 认证接口上已启用限流

