Web 攻击剖析:SQL 注入、XSS、CSRF、SSRF 与 OWASP Top 10
这一课你将能做到
- 在 DVWA 靶机上亲手复现 SQL 注入、XSS、CSRF、SSRF 四类经典攻击
- 对每一类攻击给出对应的 Spring Boot 防御代码与配置
- 用 OWASP Top 10 作为自查清单,定位自己平台的高危缺口
为什么要学
这四类攻击统治了 Web 漏洞榜单二十多年,至今仍在 OWASP Top 10 里。区别只是当年的 1' or '1'='1 换成了今天的 JSON API、ORM 拼接和云元数据接口。你写的每一行 queryWrapper、每一个接收 URL 的接口,都是攻防现场。
人话:SQL 注入是「把数据当代码执行」,XSS 是「把代码当数据混进页面」,CSRF 是「借刀杀人——用你的身份发请求」,SSRF 是「让服务器替我敲门」。
先建立图像
SQL注入: 输入 → [字符串拼接进SQL] → 数据库执行 → 数据当代码
XSS: 输入 → [原样写进HTML] → 浏览器执行 → 数据当代码
CSRF: 恶意页 → [借用户Cookie发请求] → 服务端误认合法 → 身份被借用
SSRF: 恶意URL → [服务端发请求] → 打内网/云元数据 → 服务器变跳板
共同根因只有一个:信任了不可信输入。所以防御哲学也统一:数据与代码严格分离 + 服务端不信任任何客户端。
跟着做
再次强调:以下全部操作只针对步骤 0 部署在你本机的 DVWA,这是官方授权的漏洞练习靶机。
步骤 0:启动靶场
docker run --rm -it -d -p 80:80 vulnerables/web-dvwa
# 浏览器 http://localhost,admin/password 登录,Reset Database
# DVWA Security 先保持 low,复现后再切 high 看防御差异
步骤 1:SQL 注入(SQL Injection 页)
输入 1 正常返回用户名;输入 ' OR '1'='1 返回全部用户——因为后端把输入拼进了查询:
SELECT first_name,last_name FROM users WHERE user_id = '输入';
-- 输入 1' or '1'='1 后变成:
-- ... WHERE user_id = '1' or '1'='1' → 恒真,全表返回
进阶:1' UNION SELECT user,password FROM users# 直接拖库。切到 high 难度再看,参数化后这些输入只是普通字符串。
步骤 2:XSS(XSS Reflected 页)
输入 <script>alert(document.cookie)</script>,页面直接弹回你的 Cookie。反射型靠诱导点击,存储型(XSS Stored 页)把恶意脚本存进留言板,每个访问者都中招,危害更大。
步骤 3:CSRF(CSRF 页)
<!-- 攻击页里藏一张自动提交的表单,受害者带着自己的 Cookie 访问即触发 -->
<img src="http://localhost/vulnerabilities/csrf/?password_new=hacked&password_conf=hacked" />
<!-- 受害者的浏览器自动带上 DVWA Cookie,密码被改而本人无感 -->
步骤 4:SSRF(本站文件包含页或自建演示接口)
# 假设平台有「通过 URL 拉取头像」的功能,攻击者提交:
curl "http://localhost/vulnerabilities/fi/?file=http://192.168.56.1:8080/actuator/env"
# 服务端替攻击者请求了内网地址 —— 云环境下打 http://169.254.169.254 可偷云凭证
步骤 5:写对应的 Spring Boot 防御
// ① SQL 注入:参数化(JdbcTemplate / MyBatis #{} / JPA 参数绑定)
jdbcTemplate.query("SELECT * FROM users WHERE id = ?", rowMapper, id);
// ② XSS:输出编码 + 输入校验 + 富文本白名单(OWASP Java HTML Sanitizer)
PolicyFactory policy = Sanitizers.FORMATTING.and(Sanitizers.LINKS);
String safe = policy.sanitize(untrustedHtml);
// ③ CSRF:Spring Security 默认开启 CSRF token;纯 API 可改用 SameSite=Strict Cookie
http.csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
// ④ SSRF:URL 白名单校验 + 禁内网地址
// SSRF 防御:解析后再判断(防 DNS rebinding 需二次校验)
InetAddress addr = InetAddress.getByName(new URI(userUrl).getHost());
if (!(addr.isSiteLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress())) {
throw new IllegalArgumentException("内网地址禁止访问");
}
讲清楚要点
- SQL 注入的正解是参数化,不是过滤:
#占位符让驱动把输入当纯参数传给数据库,任何引号都只是数据。靠黑名单过滤'迟早被编码绕过(%27、双重编码、宽字节)。 - XSS 防御分三道:输入校验(格式约束)、输出编码(按上下文:HTML/属性/JS/URL 各有编码方式)、CSP 响应头兜底(
Content-Security-Policy限制脚本来源)。HttpOnly Cookie 让偷到 Cookie 也没法用。 - CSRF 成立的两个条件:浏览器自动带 Cookie + 请求可被跨站伪造。防御打掉任意一个:SameSite Cookie、CSRF Token、校验 Origin/Referer。纯 JSON API 且不用 Cookie 认证时风险天然低。
- SSRF 的危害在「位置」不在「权限」:服务器在内网,它能访问数据库、注册中心、云元数据(169.254.169.254)。防御核心是出站白名单 + 解析后校验 IP + 禁用重定向跟随。
- OWASP Top 10 是思维框架不是排名榜:A01 失效的访问控制(越权)、A02 加密失败、A03 注入……用它当每个项目的自查清单,比背条目重要得多。
你可能会懵的地方
-
MyBatis 里 #{} 和 ${} 到底差在哪?
#{}预编译参数绑定,安全;${}直接字符串替换进 SQL,只允许用于白名单控制的场景(如动态排序字段:ORDER BY ${白名单校验后的列名})。 -
转义了
<和>就防住 XSS 了吗? 只防住 HTML 正文上下文。<img src="x" onerror=...>属性里、<script>var x="..."</script>JS 上下文、javascript:URL 上下文各有各的逃逸方式。按输出位置选编码,或直接用成熟库。 -
token 放 localStorage 防 CSRF,可取吗? 防 CSRF 是取,但把 XSS 的危害升级为「偷 token」。更稳的组合是 HttpOnly Cookie(SameSite)+ CSRF Token;若必须 localStorage,则 CSP + 严格输出编码是生命线。
-
接口只接收 JSON,还会有 CSRF 吗? 风险显著降低,因为攻击页无法凭空构造合法 Content-Type: application/json 的跨站请求。但若服务端对 body 解析宽松(接受表单编码),缺口仍在——收紧为强制 JSON + SameSite。
-
Spring Security 默认 CSRF 保护为什么会让 POST 全 403? 默认开启,要求每个写请求带 CSRF token。API 场景要么前端配 token 同步,要么确认无 Cookie 会话后显式关闭并改用其他防护,不能「关了就不管」。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| SQL 注入靠 replace("'", "") | 双写、编码、宽字节全可绕 | 参数化 + ORM 占位符 |
XSS 只过滤 <script> | <img onerror>、事件属性绕过 | 输出编码 + CSP + HttpOnly |
| GET 请求做状态变更 | CSRF/预取触发误操作 | 状态变更必须 POST/PUT/DELETE |
| SSRF 校验字符串 contains 内网 IP | 十进制 IP、DNS rebinding 绕过 | 解析到 IP 层校验 + 白名单 |
| 错误信息回显 SQL 原文 | 给攻击者送情报 | 统一错误页,日志只留服务端 |
卡点急救
- DVWA 注入页切 high 后打不动:high 加了 token 与参数化,属预期。先用 low/medium 理解原理,high 用于观察防御差异,不必强攻。
- XSS payload 不弹窗:浏览器 CSP 或输出编码已生效。F12 看 Console 与 Network 响应头,确认是靶机配置还是你的 payload 写法问题。
- Spring Security 开了 CSRF 后前端提交全 403:前端需先 GET 拿 token 再随表单/头带上;或改用 CookieCsrfTokenRepository + 前端读取 Cookie 回填 X-XSRF-TOKEN 头。
面试常问
-
SQL 注入的原理与根治方案? 用户输入被拼接进 SQL 语句并被执行,数据与代码边界失守。根治:参数化查询/预编译,输入校验为辅、最小权限 DB 账号兜底(即使注入也删不了库)、ORM 严禁
${}接用户输入。 -
CSRF 和 XSS 的区别与联系? CSRF 借用用户身份发请求(攻击者看不到响应),XSS 在用户浏览器执行脚本(攻击者可读取页面内容)。XSS 可以反制 CSRF 防御(读 token),所以 XSS 危害上限更高;防御上 CSRF 用 SameSite/Token,XSS 用编码/CSP。
-
讲讲 SSRF 在云环境的特殊危害。 云主机的临时凭证通过元数据接口(169.254.169.254)发放,SSRF 可直接窃取。防御:出站白名单、禁止访问链路本地地址、元数据接口要求带 Token(IMDSv2)、出网走代理统一管控。
自测三问
- 为什么参数化能根治 SQL 注入,而黑名单过滤不能?
- SameSite 属性如何阻断 CSRF?Strict 和 Lax 的取舍是什么?
- 你的项目里有哪些接口把用户提供的 URL 拿去服务端请求?SSRF 防御写了吗?
小结一句话
四类经典攻击共享同一个根因「信任了不可信输入」:SQL 靠参数化、XSS 靠输出编码、CSRF 靠断开身份自动携带、SSRF 靠出站白名单,OWASP Top 10 则是你的自查地图。
延伸
- OWASP Top 10 官方
- PortSwigger Web Security Academy(免费靶场)
- 下一课:纵深防御——WAF、IDS 与接口防刷的工程落地。