第一章:为什么安全不能"事后补"?
1.1 真实代价
| 事件 | 后果 |
|---|---|
| XSS 窃取会话 | 用户账号被接管 |
| SQL 注入 | 全库数据泄露(如 2017 Equifax) |
| IDOR(越权) | A 用户读取 B 用户的医疗记录 |
| 无速率限制 | 暴力破解管理员密码 |
原则 :安全是功能,不是附加项。
1.2 OWASP Top 10(2021)概览
| 风险 | 本篇覆盖方案 |
|---|---|
| A01: Broken Access Control | RBAC + 对象级权限校验 |
| A02: Cryptographic Failures | HTTPS + 安全 JWT + 敏感字段脱敏 |
| A03: Injection | 参数化查询 + 输入验证 |
| A04: Insecure Design | 安全开发生命周期(SDL) |
| A05: Security Misconfiguration | 安全头 + 最小权限原则 |
| A06: Vulnerable Components | 依赖扫描(safety / npm audit) |
| A07: Identification Failures | 多因素认证 + 强密码策略 |
| A08: Software Data Integrity | 未涉及(侧重供应链) |
| A09: Security Logging | 日志脱敏 + 异常行为告警 |
| A10: SSRF | URL 白名单 + 禁用内部协议 |
本篇重点解决 A01--A07、A09、A10。
第二章:前端安全加固
2.1 防御 XSS(跨站脚本)
风险场景
<!-- 危险!直接渲染用户输入 -->
<div v-html="userComment"></div>
安全方案:DOMPurify
安装:
npm install dompurify
使用:
// utils/sanitize.ts
import DOMPurify from 'dompurify'
export const sanitizeHTML = (html: string): string => {
return DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong'], // 仅允许安全标签
FORBID_ATTR: ['style', 'onerror'] // 禁用危险属性
})
}
在组件中:
<template>
<div v-html="sanitizedComment"></div>
</template>
<script setup lang="ts">
import { sanitizeHTML } from '@/utils/sanitize'
const props = defineProps<{ comment: string }>()
const sanitizedComment = sanitizeHTML(props.comment)
</script>
注意 :即使使用 Vue 的
{``{ }}插值,若后端返回 HTML 片段仍需净化。
2.2 内容安全策略(CSP)
通过 HTTP 头限制资源加载:
# nginx.conf
add_header Content-Security-Policy "
default-src 'self';
script-src 'self' 'unsafe-inline' https://cdn.example.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.yourdomain.com;
frame-ancestors 'none'; # 防点击劫持
object-src 'none';
" always;
关键指令:
script-src:禁止内联脚本(但 Vue 开发模式需'unsafe-inline',生产应移除)connect-src:限制 AJAX 请求目标frame-ancestors 'none':防止页面被嵌入 iframe
2.3 安全 Cookie 设置
虽前端不直接操作 Cookie,但需确保后端设置正确:
Set-Cookie: session_id=abc123;
HttpOnly; ← 禁止 JS 访问
Secure; ← 仅 HTTPS 传输
SameSite=Lax; ← 防 CSRF(宽松模式)
Path=/;
注意 :JWT 通常存于
localStorage,但更推荐HttpOnly Cookie(防 XSS 窃取)。
第三章:后端安全核心实践(Flask)
3.1 防 SQL 注入
错误做法
# 危险!拼接 SQL
query = f"SELECT * FROM users WHERE id = {user_id}"
正确做法:参数化查询
# SQLAlchemy ORM(自动参数化)
user = User.query.filter(User.id == user_id).first()
# 原生 SQL(显式参数)
db.session.execute(
text("SELECT * FROM users WHERE id = :user_id"),
{"user_id": user_id}
)
验证 :用
' OR '1'='1测试,应返回空或报错。
3.2 越权访问控制(Broken Access Control)
场景:用户 A 尝试访问 /api/users/123/profile(123 是用户 B)
错误实现:
@app.route('/api/users/<int:user_id>/profile')
@jwt_required()
def get_profile(user_id):
# 直接返回,未校验当前用户是否为 owner
return User.query.get(user_id).to_dict()
安全实现:
@app.route('/api/users/<int:user_id>/profile')
@jwt_required()
def get_profile(user_id):
current_user_id = get_jwt_identity()
# 关键:校验资源归属
if current_user_id != user_id:
# 或检查是否为管理员
if not current_user.is_admin:
abort(403, "Forbidden")
return User.query.get(user_id).to_dict()
进阶 :使用 对象级权限库 (如
Flask-Principal)。
3.3 CSRF 防护(针对传统表单)
注意:纯 API + JWT 通常无需 CSRF(因无 Cookie 自动携带),但若使用 Session 则需防护。
启用 Flask-WTF:
from flask_wtf.csrf import CSRFProtect
csrf = CSRFProtect(app)
前端发送 Token:
// 从 meta 标签获取
const csrfToken = document.querySelector('meta[name="csrf-token"]').content;
axios.defaults.headers.common['X-CSRFToken'] = csrfToken;
3.4 速率限制(防暴力破解)
安装 flask-limiter:
pip install flask-limiter
配置:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"]
)
@app.route("/auth/login", methods=["POST"])
@limiter.limit("5 per minute") # 登录接口严格限制
def login():
# ...
策略:
- 登录:5 次/分钟
- API:100 次/小时/IP
- 管理员接口:更严格
第四章:API 与认证安全
4.1 JWT 安全最佳实践
| 风险 | 防御措施 |
|---|---|
| Token 泄露 | 短有效期(access_token=15min)+ refresh_token 安全存储 |
| 重放攻击 | 使用 jti(JWT ID) + 黑名单(Redis 存储已注销 token) |
| 算法混淆 | 强制指定算法(如 HS256),拒绝 alg: none |
Token 注销示例:
@app.route('/auth/logout', methods=['POST'])
@jwt_required()
def logout():
jti = get_jwt()['jti']
# 加入黑名单,有效期 = 原 token 剩余时间
redis.setex(f"blacklist:{jti}", expires_in, "true")
return {"msg": "Logged out"}
验证时检查黑名单:
@jwt.token_in_blocklist_loader
def check_if_token_revoked(jwt_header, jwt_payload):
jti = jwt_payload["jti"]
return redis.exists(f"blacklist:{jti}")
4.2 敏感数据脱敏
响应中隐藏密码、身份证等:
class UserSchema(Schema):
id = fields.Int()
username = fields.Str()
email = fields.Email()
# 不输出 password_hash
# 身份证部分打码
id_card = fields.Method("mask_id_card")
def mask_id_card(self, obj):
if obj.id_card:
return obj.id_card[:6] + "****" + obj.id_card[-4:]
return None
原则 :永远不要在日志、响应、前端存储中明文出现敏感字段。
第五章:安全配置与依赖管理
5.1 安全 HTTP 头
使用 flask-talisman 自动设置:
from flask_talisman import Talisman
Talisman(app,
force_https=True,
strict_transport_security=True,
content_security_policy=csp_policy, # 同前端 CSP
referrer_policy="strict-origin-when-cross-origin",
x_frame_options="DENY"
)
5.2 依赖漏洞扫描
Python:
pip install safety
safety check -r requirements.txt
Node.js:
npm audit --audit-level high
CI 集成(GitHub Actions):
- name: Check Python dependencies
run: |
pip install safety
safety check -r requirements.txt --exit-code
策略:CI 中发现高危漏洞则失败。
第六章:自动化安全测试
6.1 静态代码分析
Python(Bandit):
pip install bandit
bandit -r ./app -f json -o bandit-report.json
常见检测项:
- 硬编码密码
- 不安全的
pickle使用 - 未验证的重定向
前端(ESLint 安全插件):
npm install -D eslint-plugin-security
.eslintrc.js:
plugins: ['security'],
rules: {
'security/detect-object-injection': 'error',
'security/detect-non-literal-fs-filename': 'error'
}
6.2 动态扫描:OWASP ZAP
启动 ZAP 扫描:
docker run -t owasp/zap2docker-stable zap-baseline.py \
-t https://your-staging-domain.com \
-r zap-report.html
CI 集成:
- name: Run ZAP Baseline Scan
run: |
docker run --network host -v $(pwd):/zap/wrk:z \
owasp/zap2docker-stable zap-baseline.py \
-t http://localhost:5000 \
-x zap-report.xml
注意:ZAP 可能触发大量请求,仅用于测试环境。
第七章:渗透测试实战(手动验证)
7.1 测试越权(IDOR)
- 用用户 A 登录,获取其
user_id=100 - 修改请求为
GET /api/users/101/profile - 预期:返回 403,而非用户 101 的数据
7.2 测试 XSS
- 在用户昵称输入
<script>alert(1)</script> - 保存后刷新页面
- 预期:脚本不执行,仅显示文本
7.3 测试 SSRF(服务器端请求伪造)
若应用支持 webhook 或图片抓取:
POST /api/fetch-image
{ "url": "http://169.254.169.254/latest/meta-data" } # AWS 元数据
预期:返回错误,禁止访问内网 IP。
防御代码:
from urllib.parse import urlparse
import ipaddress
def is_safe_url(url):
try:
result = urlparse(url)
# 检查是否为内网 IP
addr = ipaddress.ip_address(socket.gethostbyname(result.hostname))
return not addr.is_private and not addr.is_loopback
except:
return False
第八章:CI/CD 安全门禁
8.1 GitHub Actions 安全工作流
name: Security Scan
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v4
with: { python-version: '3.11' }
- name: Install deps
run: pip install -r requirements.txt -r requirements-dev.txt
- name: Bandit Scan
run: bandit -r ./app --exit-zero -f json | tee bandit.json
- name: Safety Check
run: safety check -r requirements.txt --exit-code
- name: ESLint Security
run: npm run lint:security
- name: Fail on High Risk
run: |
if grep -q '"severity": "HIGH"' bandit.json; then
echo "High risk found!"; exit 1
fi
效果:PR 中若含高危漏洞,无法合并。
第九章:安全开发流程(SDL)
将安全融入开发全周期:
| 阶段 | 活动 |
|---|---|
| 需求 | 威胁建模(STRIDE) |
| 设计 | 安全架构评审 |
| 编码 | 安全编码规范 + SAST |
| 测试 | DAST + 渗透测试 |
| 发布 | 依赖扫描 + 配置审计 |
| 运维 | 日志监控 + 应急响应 |
工具链:
- 威胁建模:Microsoft Threat Modeling Tool
- SAST:Bandit, Semgrep
- DAST:OWASP ZAP, Burp Suite