242 lines
6.8 KiB
Markdown
242 lines
6.8 KiB
Markdown
# 026-secure-coding.mdc (Deep Reference)
|
||
|
||
> 该文件为原始详细规范归档,供 Tier 3 按需读取。
|
||
|
||
---
|
||
|
||
|
||
# 🔐 Secure Coding Standards (Project CodeGuard)
|
||
|
||
> 安全不是事后审查,而是编码时的默认选择。
|
||
|
||
## ❌ 禁用加密算法(绝对禁止)
|
||
|
||
以下算法已被证实不安全,**禁止在任何新代码中使用**:
|
||
|
||
| 类型 | 禁用 | 替代 |
|
||
|------|------|------|
|
||
| 哈希 | MD2、MD4、**MD5**、SHA-0、**SHA-1** | SHA-256、SHA-3 |
|
||
| 对称加密 | RC2、RC4、Blowfish、DES、3DES、**AES-ECB** | **AES-GCM**、ChaCha20-Poly1305 |
|
||
| 密钥交换 | 静态 RSA、匿名 DH | ECDHE (X25519) |
|
||
|
||
```php
|
||
// ❌ 禁止
|
||
$hash = md5($password);
|
||
$hash = sha1($data);
|
||
openssl_encrypt($data, 'AES-128-ECB', $key);
|
||
|
||
// ✅ 正确
|
||
$hash = password_hash($password, PASSWORD_ARGON2ID);
|
||
$hash = hash('sha256', $data);
|
||
openssl_encrypt($data, 'AES-256-GCM', $key, 0, $iv, $tag);
|
||
```
|
||
|
||
## 💉 注入防护
|
||
|
||
### SQL 注入
|
||
- **100% 使用参数化查询**,禁止字符串拼接 SQL
|
||
- 使用 Hyperf ORM 或 PDO 绑定参数
|
||
|
||
```php
|
||
// ❌ 禁止
|
||
$result = Db::select("SELECT * FROM users WHERE name = '{$name}'");
|
||
|
||
// ✅ 正确
|
||
$result = Db::select('SELECT * FROM users WHERE name = ?', [$name]);
|
||
// 或
|
||
User::where('name', $name)->first();
|
||
```
|
||
|
||
### OS 命令注入
|
||
- 优先使用内置函数替代 shell 调用
|
||
- 禁止将用户输入直接传入 `exec()`、`system()`、`shell_exec()`
|
||
|
||
```php
|
||
// ❌ 禁止
|
||
exec("convert {$userFile} output.png");
|
||
|
||
// ✅ 正确(使用内置库)
|
||
$image = new Imagick($sanitizedPath);
|
||
```
|
||
|
||
### TypeScript Prototype Pollution
|
||
- 使用 `Map` / `Set` 替代对象字面量存储用户数据
|
||
- 合并对象时拦截 `__proto__`、`constructor`、`prototype` 键
|
||
|
||
```typescript
|
||
// ❌ 禁止
|
||
function merge(target, source) {
|
||
Object.assign(target, source) // 可能导致 prototype pollution
|
||
}
|
||
|
||
// ✅ 正确
|
||
const safe = Object.create(null)
|
||
const blocked = ['__proto__', 'constructor', 'prototype']
|
||
Object.keys(source).filter(k => !blocked.includes(k)).forEach(k => { safe[k] = source[k] })
|
||
```
|
||
|
||
## 🔑 密码存储
|
||
|
||
```php
|
||
// ❌ 禁止(bcrypt 有 72 字节截断限制)
|
||
$hash = password_hash($password, PASSWORD_BCRYPT);
|
||
|
||
// ✅ 推荐(Argon2id:更安全,无长度限制)
|
||
$hash = password_hash($password, PASSWORD_ARGON2ID, [
|
||
'memory_cost' => 65536, // 64 MiB
|
||
'time_cost' => 2,
|
||
'threads' => 1,
|
||
]);
|
||
|
||
// 验证(常量时间比较,防时序攻击)
|
||
$valid = password_verify($input, $hash);
|
||
```
|
||
|
||
## 🔒 访问控制(IDOR / Mass Assignment 防护)
|
||
|
||
### IDOR 防护
|
||
- 永远不要只凭用户传入的 ID 查询资源,必须附加所有权校验
|
||
|
||
```php
|
||
// ❌ 禁止 — 用户可访问任意 ID 的数据
|
||
$order = Order::find($request->input('id'));
|
||
|
||
// ✅ 正确 — 限定当前用户范围
|
||
$order = $this->user->orders()->findOrFail($request->input('id'));
|
||
```
|
||
|
||
### Mass Assignment 防护
|
||
- Hyperf FormRequest 必须声明 `rules()` 白名单
|
||
- Model 必须使用 `$fillable` 而非 `$guarded = []`
|
||
|
||
```php
|
||
// ❌ 禁止
|
||
$user->fill($request->all());
|
||
|
||
// ✅ 正确(只允许明确字段)
|
||
$user->fill($request->only(['name', 'email', 'avatar']));
|
||
```
|
||
|
||
## 🍪 Session 和 Cookie 安全
|
||
|
||
```php
|
||
// 登录成功后必须轮转 Session ID(防 Session Fixation)
|
||
session_regenerate_id(true);
|
||
|
||
// Cookie 必须设置安全属性
|
||
setcookie('session', $id, [
|
||
'secure' => true, // 仅 HTTPS
|
||
'httponly' => true, // 禁止 JS 访问
|
||
'samesite' => 'Strict', // 防 CSRF
|
||
'path' => '/',
|
||
]);
|
||
```
|
||
|
||
- Session 超时:高风险操作 5 分钟,常规 30 分钟,绝对上限 8 小时
|
||
- **禁止在 `localStorage` / `sessionStorage` 存储 Session Token**(XSS 可窃取)
|
||
|
||
## 🌐 客户端安全(Vue 3 / TypeScript)
|
||
|
||
### XSS 防护
|
||
```typescript
|
||
// ❌ 禁止 — 直接使用 v-html 且无净化
|
||
// <div v-html="userContent"></div>
|
||
|
||
// ✅ 正确 — 使用 DOMPurify 净化
|
||
import DOMPurify from 'dompurify'
|
||
const clean = DOMPurify.sanitize(userContent, {
|
||
ALLOWED_TAGS: ['b', 'i', 'p', 'a', 'ul', 'li'],
|
||
ALLOWED_ATTR: ['href'],
|
||
})
|
||
// <div v-html="clean"></div>
|
||
|
||
// ❌ 禁止 — 动态代码执行
|
||
eval(userCode)
|
||
new Function(userCode)()
|
||
setTimeout(userString, 100)
|
||
|
||
// ❌ 禁止 — 直接 DOM 写入
|
||
element.innerHTML = userInput
|
||
document.write(userInput)
|
||
```
|
||
|
||
### 外链安全
|
||
```html
|
||
<!-- ✅ 所有 target="_blank" 必须加防护 -->
|
||
<a href="..." target="_blank" rel="noopener noreferrer">外链</a>
|
||
```
|
||
|
||
### CSRF 防护
|
||
- 所有状态变更请求(POST/PUT/DELETE/PATCH)必须携带 CSRF Token
|
||
- 禁止用 GET 请求执行状态变更操作
|
||
|
||
## 📁 文件上传安全
|
||
|
||
```php
|
||
// ✅ 文件上传安全检查清单
|
||
|
||
// 1. 白名单扩展名(不是黑名单)
|
||
$allowed = ['jpg', 'png', 'gif', 'pdf'];
|
||
$ext = strtolower(pathinfo($file->getClientFilename(), PATHINFO_EXTENSION));
|
||
if (!in_array($ext, $allowed)) throw new ValidationException('不允许的文件类型');
|
||
|
||
// 2. 验证 magic bytes(不信任 Content-Type)
|
||
$finfo = new finfo(FILEINFO_MIME_TYPE);
|
||
$mimeType = $finfo->file($tmpPath);
|
||
|
||
// 3. 生成随机文件名(不使用用户提供的文件名)
|
||
$safeName = Str::uuid() . '.' . $ext;
|
||
|
||
// 4. 存储在 webroot 外
|
||
$storagePath = BASE_PATH . '/storage/uploads/' . $safeName;
|
||
|
||
// 5. 设置文件大小限制
|
||
if ($file->getSize() > 10 * 1024 * 1024) throw new ValidationException('文件过大');
|
||
```
|
||
|
||
## 📊 安全日志规范
|
||
|
||
```php
|
||
// ❌ 禁止记录敏感信息
|
||
Log::info('用户登录', ['password' => $password, 'token' => $token]);
|
||
|
||
// ✅ 正确 — 只记录安全信息,用 hash 标识 session
|
||
Log::info('用户登录', [
|
||
'user_id' => $userId,
|
||
'ip' => $request->getServerParams()['REMOTE_ADDR'],
|
||
'user_agent' => substr($request->getHeaderLine('user-agent'), 0, 200),
|
||
'session_id' => substr(hash('sha256', $sessionId), 0, 16), // 前16位,不泄露原值
|
||
'timestamp' => date('c'),
|
||
]);
|
||
```
|
||
|
||
## ⚙️ API 安全
|
||
|
||
- **禁止在 URL 参数中传递敏感数据**(会出现在日志里)
|
||
- GraphQL:生产环境禁用 introspection
|
||
- SSRF:所有外部 HTTP 请求必须验证目标域名白名单
|
||
|
||
```php
|
||
// SSRF 防护示例
|
||
$allowedDomains = ['api.trusted.com', 'cdn.trusted.com'];
|
||
$parsedUrl = parse_url($userProvidedUrl);
|
||
if (!in_array($parsedUrl['host'], $allowedDomains)) {
|
||
throw new SecurityException('不允许的目标地址');
|
||
}
|
||
// 还需阻断私有 IP 范围:10.x, 172.16.x, 192.168.x, 127.x
|
||
```
|
||
|
||
## ✅ 编码时自查清单
|
||
|
||
每次提交前确认:
|
||
- [ ] 无 MD5/SHA1/DES/AES-ECB 使用
|
||
- [ ] SQL 查询 100% 参数化
|
||
- [ ] 资源查询已限定所有权范围(无 IDOR 风险)
|
||
- [ ] 无 `$request->all()` 直接绑定模型
|
||
- [ ] Cookie 包含 Secure + HttpOnly + SameSite 属性
|
||
- [ ] `v-html` 使用了 DOMPurify 净化
|
||
- [ ] 文件上传使用 UUID 文件名 + magic bytes 验证
|
||
- [ ] 日志未包含明文密码/Token/Session ID
|
||
|
||
> 📚 深度参考:`.cursor/skills/security-audit/references/codeguard/`
|