网上关于Web漏洞的科普文章一抓一大把,但很多不是讲得太浅,就是开篇先抛出一堆让人劝退的高深术语。最近几个月有好几个做开发的朋友问我要一份能直接看懂的漏洞入门资料,我觉得与其零散地讲单个漏洞,不如先把最经典的四种Web漏洞放在一起掰开揉碎讲一遍:SQL注入、跨站脚本(XSS)、跨站请求伪造(CSRF)、文件上传漏洞。这四种漏洞几乎覆盖了从服务端到客户端的主要攻击路径,也是日常安全测试报告里出现频率最高的四类。无论你是后端开发、前端开发、测试还是运维,把这四种漏洞理解透之后,再去看更复杂的安全问题会轻松很多。这篇文章我会把每类漏洞的原理、利用条件、危害、防御措施一次讲透,并把我这些年做安全评估时踩过的坑、总结出的排查经验也一并分享出来。
1. 为什么是这四种:高频、高危与学习路径
1.1 四种漏洞的关系与差异
先讲一句可能让新人安心的结论:这四种Web漏洞并不是彼此孤立的,相反,它们经常组合出现。SQL注入发生在服务端的数据访问层,攻击者通过篡改SQL语句让数据库执行非预期操作;XSS发生在浏览器端,攻击者借助网页对用户数据的信任向其他用户注入脚本;CSRF利用浏览器自动携带Cookie的机制,诱导受害者发出攻击者指定的请求;文件上传则是在服务端的文件处理环节上做文章,一旦失效,可能直接导致服务器被控制。
用一个简单的分类方式帮大家记忆:SQL注入和文件上传漏洞属于“服务端没把外部输入当外人”,攻击目标是服务器本身;XSS属于“服务端把不可信数据当成了可信页面内容”,攻击目标变成了其他用户;CSRF属于“服务端太信任浏览器带来的身份认证结果”,攻击目标是用户的操作行为。为了更直观,我整理了一个对比表:
| 漏洞 | 攻击对象 | 核心根因 | 典型危害 |
|---|---|---|---|
| SQL注入 | 服务器/数据库 | 用户输入被拼接进SQL语句 | 拖库、篡改数据、提权 |
| XSS | 其他用户浏览器 | 不可信数据未做输出编码 | 会话劫持、钓鱼、篡改页面 |
| CSRF | 已登录用户 | 请求来源未被校验 | 盗用身份执行操作 |
| 文件上传 | 服务器 | 文件类型/内容校验不严 | 上传WebShell、控制服务器 |
这里要多说一句:SQL注入和文件上传一旦被利用,危害非常直接,数据库可能被拖走,服务器可能被控制,损失可以量化;XSS和CSRF属于“借力打力”,攻击者要利用其他用户的身份或浏览器权限,虽然中间链路长,但这类漏洞在信息发布类系统里的可利用场景非常广泛。很多安全评估报告里最扎眼的几条漏洞,往往不是那些最复杂的问题,而是这几类基础漏洞。
1.2 理解这四类漏洞需要哪些前置知识
如果你是零基础,不要慌,你只需要先掌握三件事:HTTP请求的基本结构、Cookie与Session的作用、前端表单和后端接口的交互方式。HTTP请求本质上就是你给服务端递的一张“点菜单”,里面包含请求行(方法加路径)、请求头(浏览器信息、Cookie、内容类型等)、请求体(提交的参数或文件内容)。Web漏洞的产生,绝大多数都是因为这些“点菜单”里的某些内容没有被服务端正确识别和处理。
Cookie和Session决定了服务器怎么记住“你是谁”。从安全测试的角度看,很多漏洞利用都是围绕“身份凭证”展开的:XSS想偷偷拿到别人的Cookie,CSRF想借用户已经认证过的Cookie发起请求,SQL注入想绕过后端校验去数据库里改东西。理解这一点,后面所有内容都会顺很多。
实操上,我建议自己搭一个本地靶场。不管是用虚拟机跑一个开源靶场环境,还是直接在本地用一套简单的PHP或Node项目来练习都是可以的。不推荐直接拿线上系统练手,一是法律风险,二是线上环境干扰因素太多,不利于观察漏洞的完整链路。对于纯新人,先在本地把请求和响应看明白,再开始研究漏洞,效率会高很多。
2. SQL注入:最经典的数据库入口漏洞
2.1 SQL注入是怎么发生的
SQL注入的触发点非常典型:服务端在拼接SQL语句时,直接把用户输入放进了代码里。比如某个登录功能,原本想执行的是“查一下用户名和密码匹配不匹配”,后端代码可能是这样组织的:
# 千万不要这样写,这是错误示范 sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'"当用户输入的用户名是admin' or '1'='1,密码随便写的时候,整条语句会变成:
SELECT * FROM users WHERE username = 'admin' or '1'='1' AND password = 'xxx'因为or '1'='1'这个条件是恒真的,数据库就会返回用户表里的第一行数据,登录校验被绕过。生活里可以把它类比成:你给保安报了一个“一定能通过的暗号”,保安不管你是谁,只认暗号。
根本原因其实只有一句话:数据越过了代码边界,被当成了SQL指令的一部分。所以任何把前端参数直接拼进SQL字符串的做法都有注入风险,不管你用的是MySQL、Oracle还是PostgreSQL。很多开发新人觉得“只要过滤一下单引号就行”,但绕过方式实在太多,后面我会讲为什么不能靠过滤来根治。
2.2 常见注入点与初步判定方法
SQL注入最常见的入口包括:登录框、搜索框、URL上的参数(比如?id=1)、排序字段、分页参数。判断一个参数是不是注入点,最稳妥的思路不是直接丢一堆payload猛试,而是观察“输入内容是否影响了查询结果的结构变化”。
一个非常可靠的基础判定方法是:先提交id=1,页面正常;再把id=1改成id=1 and 1=1,如果页面仍然正常,然后再把参数改成id=1 and 1=2,如果页面报错或显示内容变了,说明这个数字型参数非常有可能被直接拼进了SQL。再举一个字符串型的例子:在搜索框里随便输入一个词,结果正常;输入词'多了一个单引号,页面报错或空白,说明参数可能被拼进了SQL,单引号打破了原有语法。
需要特别提醒:上面说的判定方法只建议在你自己搭建的本地靶场里做,不要拿到公网系统上去试。判断注入的目的是为了防守修复,不是拿来搞破坏。在真实渗透测试中,必须先取得书面授权,否则后果很严重。
2.3 本地靶场里的一次完整分析
早前我在做某个内部系统的安全性评估时,遇到过一个典型的数字型注入点。页面加载新闻详情时带了一个article_id参数,我按照上面的思路先测试article_id=1,正常;然后又测article_id=1 and 1=2,页面返回空。这时候基本可以确认有漏洞。
为了知道查询返回了哪些列,需要使用ORDER BY来判断列数:比如article_id=1 ORDER BY 1、ORDER BY 2、ORDER BY 3,直到页面报错,说明列数就是报错前最后一个合法数字。这个数字决定了后面联合查询的语法是否正确。确定列数后,就能在本地靶场里尝试把查询位置对应的参数替换成“数据库查询表达式”,从而把当前数据库、版本、表名一个一个问出来。
整个过程的核心逻辑并不复杂,难点在于很多人对SQL语法不熟,容易在单引号、括号、注释符这些细节上卡住。我当时踩过的坑是:忘记注释掉SQL语句后面的残留片段,导致语法错误。在真实的拼接场景中,SQL语句后面可能还有WHERE条件或LIMIT子句,所以通常需要用--或#把后面的内容注释掉,具体用哪一种,要看数据库方言。
2.4 防御:参数化查询与纵深设计
防御SQL注入最根本的方法,是让用户输入永远只作为“数据”传递,不参与SQL结构的拼接。最成熟的做法是使用参数化查询,也就是预编译语句。在绝大多数语言的数据库驱动里,你只需把SQL语句里的参数位用占位符表示,后续再把值传进去,数据库驱动会帮你避免用户输入破坏SQL结构。
# 推荐方式:参数化查询 cursor.execute("SELECT * FROM users WHERE username = ? AND password = ?", (username, password))这里的原理是:SQL语句在执行前已经被数据库解析并确定了结构,用户输入即使包含再多的单引号、注释符,也只能被当作字符串值处理,无法再改变执行计划。
除了参数化查询,还要做纵深防御:数据库账号不要用管理员权限跑业务,尽量按库按表分配最小权限;报错信息不要直接抛给前端,避免给攻击者提供语法线索;如果某些动态排序、动态列名的场景确实无法参数化,那就用白名单映射,把允许的列名或排序方向写死在代码里,用户只能传index,再根据index从配置表里查对应的规则。这样即便某个点被绕过,影响范围也被限制住了。
3. XSS:潜伏在网页里的前端劫持
3.1 XSS的本质与三种类型
XSS全称是跨站脚本攻击,它的核心问题是:服务端把用户提交的数据原样当成网页内容输出,浏览器也没有把它当作普通文本,而是当作可执行的HTML或JavaScript执行了。简单说,攻击者在一个允许输入内容的地方写入一段脚本,后续其他用户访问这个页面时,浏览器就在受害者的会话上下文里执行了这段脚本。
这段脚本能干什么,就看你愿意让它干什么:读取当前页面的Cookie、发送异步请求、修改页面内容、诱导用户点击,甚至把数据传到外部。所以XSS的受害者不是服务器,而是访问页面的用户。相比于SQL注入,XSS的危害看起来不那么“直接”,但在涉及真实用户数据的系统里,XSS往往能造成大范围的会话劫持和钓鱼事件。
按触发和存储方式,XSS一般分成三类:反射型、存储型、DOM型。反射型是攻击者把脚本放在链接的参数里,服务端把参数值直接渲染到页面,受害者一点链接就触发,脚本不在服务器上驻留;存储型是把脚本存在数据库里,所有访问该页面的用户都会触发;DOM型是不经过服务端渲染,而是前端JavaScript直接操作DOM时把不可信数据写进了HTML。三类中存储型危害最稳定,因为它触发人数多、持续时间长。
3.2 从留言板到会话劫持的完整链路
我拿“留言板”这个场景来说。假如一个留言板把用户提交的昵称直接放在页面标题里,没有做任何编码,攻击者提交的昵称是“一段用来获取Cookie的脚本”。当管理员或普通用户查看留言列表时,这个脚本就执行了,Cookie就悄悄发到攻击者控制的测试收集接口。
很多人问:Cookie被偷走有什么严重后果?因为Cookie经常就是会话凭证,攻击者拿到之后可以冒充这个用户发起请求,修改密码、发表内容、操作后台,具体危害取决于用户权限。所以XSS最常被用来配合CSRF,或者直接做会话劫持。在建设完善的系统里,攻击者可能还需要同时拿到CSRF Token,但在老旧系统里,一个Cookie往往就够用了。
这里必须强调:上面说的链路,我只在本地靶场环境里演示过,而且用的是一个自己搭的、只用于学习的测试接口。不要去真实网站上尝试任何类似操作。安全测试的价值在于理解原理、提前修补,而不是去证明自己“能攻破某系统”。
3.3 为什么过滤脚本总是失效
很多初学开发的同事问我:我把<script>标签替换掉不就行了吗?实际上这只能挡住最老实的攻击者。攻击者可以把标签写成<scr<script>ipt>,利用过滤后再解析的特性绕过;也可以用<img src=x onerror=alert(1)>这种不需要script标签的载荷;还可以通过HTML实体编码、大小写混写、Unicode编码绕过关键词匹配。过滤规则写得很长,但攻击者总能从解析差异中找到缝隙。
所以安全界有一条近乎共识的经验:不要试图用黑名单去穷举恶意输入,因为你永远想不到下一个绕过姿势。正确的思路是区分“输入”和“输出”两个环节,在输入侧做合理的长度与范围校验,在输出侧根据上下文做严格的编码或转义,确保用户数据即便被原样输出,也不会被浏览器当作代码执行。
3.4 防御:输出编码、HttpOnly与CSP
具体怎么做?首先要分清输出位置:如果数据输出在HTML标签内部,要把<、>、&、单双引号等字符转换成对应的HTML实体;如果数据输出在JavaScript字符串里,就要做JavaScript转义;如果输出在URL里,要做URL编码。前端框架现在普遍默认带转义,但一定要小心v-html、dangerouslySetInnerHTML这类“绕过转义”的接口,使用它们时必须有额外的安全校验。
第二道防线是给会话Cookie加HttpOnly属性,这样浏览器端的JavaScript无法读取到Cookie值,XSS再怎么执行也拿不到会话凭证。注意,HttpOnly并不是防御XSS本身,而是降低XSS造成会话劫持的风险。
第三道防线是内容安全策略,通过响应头限制页面可以加载和执行的资源来源。比如只允许脚本从本站加载、禁止内联脚本,攻击者就算成功注入了一段脚本,也会被浏览器策略拦下。CSP的配置容易误伤正常功能,需要根据项目实际情况逐步收紧,但从安全角度看,它是非常值得投入的防御措施。
4. CSRF:借用户之手发起的“合法”请求
4.1 CSRF的原理与触发前提
CSRF跨站请求伪造,理解的难点在于“这个请求是受害者自己发出的”。假设用户已经登录了某个网站,浏览器里保存了该网站的Cookie,攻击者在另一个网站上放置了一个表单或链接,诱导用户点击。当浏览器带着该网站的Cookie向目标网站发起请求时,目标网站的服务端看到的是一个“已登录用户”的合法请求,于是执行了攻击者预设的操作。
可以把CSRF理解成:有人提前写好了一张“转账申请单”,然后骗一个有权力的员工在上面签了字。签字是真的,但签字人并不知道自己签了什么。这个漏洞的本质,是服务端没有确认“当前请求是不是用户主动发出的”,只确认了“请求是不是带着合法身份凭证”。
触发CSRF有两个重要前提:一是受害者已经在目标站点登录,且浏览器保存了有效的认证凭据;二是目标站点的关键操作请求可以仅凭Cookie或Basic认证等自动附加的凭据完成,不需要额外的自定义头或二次校验。如果某个接口要求自定义请求头或者需要二次验证,CSRF的利用难度会大很多。
4.2 从恶意表单到账户接管
举个最常见的例子:某系统有一个“修改绑定邮箱”的接口,正常通过POST请求提交email=新地址,服务端只验证用户的登录状态,不校验来源。攻击者在自己的页面里嵌一个自动提交的表单:
<form action="https://某系统的接口地址/change-email" method="POST"> <input type="hidden" name="email" value="attacker@example.com"> </form> <script>document.forms[0].submit();</script>当受害者访问攻击者的页面时,表单自动向目标系统提交。因为浏览器会自动携带目标系统的Cookie,服务端一看用户已登录,就真的把邮箱改了。这一步之后,攻击者还可以利用“忘记密码”功能把账户完全接管。
这里有个概念要解释清楚:浏览器同源策略会限制JavaScript读取跨域响应,但并不阻止“发出请求”。所以CSRF的核心矛盾不在于跨域读取信息,而在于利用浏览器自动携带Cookie的机制发起状态改变请求。现在有些前端框架会默认带X-Requested-With头,但这并不是可靠的防御,因为攻击者同样可以用表单发起普通请求,框架不会自动给表单请求加这个头。
4.3 防御:Token、SameSite与二次校验
CSRF的防御思路很清晰:让服务端能够区分“用户主动发起的请求”和“被第三方页面借力发起的请求”。
最常用的是CSRF Token:服务端在渲染页面时生成一个随机Token,放进表单或Cookie;提交请求时服务端校验Token是否合法。由于攻击者的页面无法读取目标站点的Token(同源策略会阻止跨域读取),所以无法伪造出带正确Token的请求。注意Token不要放在GET请求的URL里,否则容易被浏览器历史、日志或Referer泄露。
更省心的一个方案是设置Cookie的SameSite属性。SameSite=Lax时,浏览器只在跨站子资源请求或导航到目标站点的GET请求中携带Cookie,跨站POST表单请求则不携带,这能挡住大量CSRF。但要注意,它依赖浏览器的实现,老版本浏览器不认这个属性。所以生产环境里还是应当把Token校验作为主打方案,SameSite作为增强。
还有最后一道兜底:在修改密码、绑定手机、转账这一类敏感操作上加二次校验,比如输入原密码、短信验证码、校验用户交互状态。这样做既能防CSRF,也能防一部分撞库和撞操作的风险。
5. 文件上传:从“传个文件”到“控制服务器”
5.1 文件上传漏洞的利用逻辑
文件上传功能在系统里太常见了:用户头像、产品图片、附件简历、导入Excel。上传本身不是问题,问题在于服务端“收下文件之后做的事”。很多系统的上传逻辑是:接收文件、保存到指定目录、拼一个访问URL返回给前端。如果这个过程中没有严格校验文件内容和类型,攻击者就可能上传一个包含服务端代码的脚本文件,然后通过URL直接访问,让脚本在服务器上执行。
后续攻击者能做什么,就取决于脚本权限了:读取源代码、连接数据库、执行系统命令、横向移动,甚至把整个服务器变成跳板。所以文件上传漏洞的危害是所有漏洞里最直接的一档:它可能让问题从“Web应用层”直接升级到“服务器层”。
很多人以为只有PHP站点才有这个问题,其实Java、Python、Node都一样,只是落地脚本的语言不同。逻辑是一样的:服务端把“可执行代码文件”放进了可被HTTP访问的目录,等于亲手教攻击者怎么运行代码。
5.2 常见的绕过手法与防御盲区
理解了利用逻辑,我们再反推防御。常见的历史绕过手法有这么几类:改Content-Type头伪装成图片;把文件后缀改成.php5、.phtml等服务端可能仍然解析的变体;在图片文件的尾部追加一段脚本,再结合文件包含漏洞触发;利用操作系统的文件名解析特性,比如上传shell.php.或shell.php%00.jpg;双写后缀绕过删除规则;以及大小写绕过。这些手法本质上都是在绕过服务端对文件类型的判断,但如果服务端能确认文件真实内容,而不是只信客户端自己声明的类型,绝大多数绕过都不会成功。
这里想特别讲一个防御盲区:很多系统只检查了上传文件的扩展名,却没有检查目录是否可执行。这样即便文件后缀被校验得死死的,攻击者也可能通过其他漏洞写入脚本到可执行目录。所以文件上传的防御必须是多个环节的组合拳,不能只盯着“扩展名是否正确”这一个点。
5.3 防御:白名单、重命名、目录权限与内容校验
我的建议是分四层处理。第一层,扩展名白名单,只允许图片或指定文档的后缀,比如jpg、jpeg、png、gif、pdf,拒绝所有可执行后缀。不要用黑名单,因为后缀变体太多。第二层,内容校验,通过读取文件头(如图片的魔数)、解析图片的真实宽高,确认它确实是个图片。第三层,随机化文件名并重命名,让攻击者无法预测最终上传路径。第四层,把上传目录和解析目录分离,上传后的文件放在独立的静态存储目录,该目录禁止执行任何服务端脚本,并配置下载接口而不是直接HTTP暴露。
另外,对上传文件的大小、数量也要做限制,防止攻击者用海量文件消耗存储。对于需要在线预览的文档,最好用独立的文档服务或转换为图片后再展示,这样即便文件内容有恶意负载,也没有机会在应用层被执行。
提示:文件上传功能上线前,最稳妥的验证方法是上传一个最简单的探针脚本,看看它能不能被直接访问并执行。如果不行,再继续检查日志里的请求路径和解析结果。这是低成本、高回报的回归测试手段。
6. 四种漏洞的排查技巧与实操总结
6.1 常见问题速查表
把四种漏洞放在一起,整理成一张速查表,方便你在代码评审或安全自检时快速定位问题:
| 漏洞 | 典型现象 | 最可能的根因 | 快速排查方法 |
|---|---|---|---|
| SQL注入 | 参数带单引号时报错,页面内容随条件变化 | 字符串拼接SQL,未使用参数化 | 全局搜索SQL拼接关键字,检查动态SQL |
| XSS | 页面出现弹窗或资源请求异常 | 用户数据未做输出编码 | 搜索innerHTML、v-html、模板字符串输出用户内容 |
| CSRF | 关键操作无Token、无来源校验、Cookie未设SameSite | 业务请求仅依赖Cookie鉴权 | 检查改密、绑定手机、转账等接口的校验方式 |
| 文件上传 | 上传目录可直接返回脚本文件并执行 | 扩展名/文件内容校验不严,上传目录可执行 | 测试上传非白名单文件,观察响应与解析结果 |
这张表不意味着满足“快速排查”就没问题,但至少能帮你在业务代码里快速锁定高风险点。排查时不要只看报错,还要看请求日志和响应头,多个线索一起交叉验证,判断会更准确。
6.2 项目上线前的快速自检清单
我每次做安全评审,会习惯性过一遍这份清单:
- 所有SQL操作是否都使用了参数化查询或ORM映射?有没有任何动态拼接?
- 所有输出到HTML、JavaScript、URL的用户数据,是否在对应上下文做了编码或转义?
- 是否存在使用
innerHTML、v-html等直接渲染不可信数据的写法? - 敏感操作的接口是否校验CSRF Token?Cookie是否设置了
HttpOnly和SameSite? - 文件上传是否有扩展名白名单、内容校验、随机文件名、目录权限分离?
- 错误信息是否对用户隐藏了数据库、文件路径等细节?
- 后台管理页是否有额外的访问控制和审计日志?
清单看起来简单,真正落实起来需要每个环节都有对应的测试用例。很多团队上线前只做功能测试,不做安全测试,等到出了问题再回头补,成本要高得多。比较务实的做法是把这份清单塞进CI流程里,至少把最基础的自动化扫描跑起来,人工再针对高危功能点做一轮复查。
6.3 我的实操心得与踩坑记录
最后分享几个我从实际代码审计里总结出来的体会,不算高深,但都是踩过的坑。
第一,别把漏洞修复做成“打补丁”。比如发现SQL注入后只在某个方法里加了一层过滤,但其他类似的查询还是拼接的,这种修复是无效的。正确做法是先定位根因,再把编码规范固定下来,用统一的数据访问层处理所有数据库交互。
第二,看漏洞要看完整链路,不要盯着单个函数。比如XSS问题,如果只在输入侧过滤了特殊字符,你要想一下输出侧的上下文,是不是还有某个字符串拼接绕过过滤;CSRF问题,不要只看有没有Token,还要看Token是否被放进了URL、Token是否可预测。
第三,安全测试一定要回归验证。修复完一个问题后,我会把当初构造的测试样例原样再跑一遍,同时再试几种绕过变体。因为实际项目中经常会遇到“修了过滤却引入了新的拼接方式”这种奇特情况。
还有一个很小的经验:给用户展示的报错信息越少,系统越安全。不仅是SQL注入,文件上传、CSRF等很多漏洞都会在报错信息里泄露细节。所以统一格式化错误提示,在开发环境才输出详细堆栈,线上环境只显示一句话,能挡掉不少低成本的探测。
做安全相关工作久了,你会发现这四种漏洞只是Web安全领域的入门门槛,但它们背后揭示的问题——不信任外部输入、不信任浏览器凭据、不信任文件名——是相通的。把这些思路真正内化成编码习惯,比背一百个攻击payload都管用。希望这篇详解能帮你把最基础的四类Web漏洞理清,在写代码和做测试的时候,多一份“这数据能不能被利用”的判断力。