做了快十年Web开发,也断断续续扛了七八年线上告警,我对Web攻击这件事最大的感受是:真正让你睡不着觉的,往往不是那些听起来很酷的0day,而是排行榜上翻来覆去那几张老面孔。前两年帮一家电商公司做上线前的安全巡检,登录接口被人用最基础的SQL注入 payload 刷了十几万次,WAF日志里全是' or 1=1--这种教科书级手法。你说攻击者水平高吗?不高。但就是这么老套的攻击,依然能一晚上试出好几个可注入点,因为业务代码里总有人图省事拼字符串。
所以这次我打算把这几年在实战里遇到的、以及行业公认高发的Web攻击整理成一份清单。不会只列概念,重点讲清楚每种攻击为什么会成功、在你自己的系统里最可能藏在哪个角落、以及用什么样的防御姿势才能真的挡住。看完你可以直接拿这份清单去对照自己的项目做一轮自查,比单纯背OWASP Top 10有用得多。
1. 为什么要反复聊这十个:从攻击者的视角看Web系统
很多开发兄弟看到"十大攻击"就觉得是老生常谈,但我想先说一个反直觉的事实:攻击者从来不会因为你用的是新框架、新语言就绕道走。他们找的是业务逻辑里永恒不变的东西——输入不可信、权限校验缺失、组件依赖过老。这些东西跟技术栈没太大关系,十年前写的PHP站和今天用Go写的微服务,犯的错可能是同一个。
我自己做攻防演练的时候,习惯把攻击路径分成四类,方便梳理:
| 攻击类别 | 典型攻击 | 攻击目标 |
|---|---|---|
| 注入类 | SQL注入、命令注入、反序列化 | 篡改服务端执行逻辑 |
| 前端对抗类 | XSS、CSRF | 劫持用户会话、骗取浏览器信任 |
| 资源滥用类 | DDoS、CC攻击、慢速HTTP | 耗尽服务资源 |
| 信任链利用类 | SSRF、文件上传、路径遍历、XXE | 突破网络边界、读取敏感文件 |
这四类基本覆盖了我日常收到告警的90%以上。下面的内容就按这个逻辑展开,每一类里挑最典型的攻击方式,讲透原理和防御。你别把它当成安全课程,就当是同事之间聊项目时互相提醒:"哎,你那个上传接口得注意啊。"
2. 注入家族的常青树:SQL注入与命令注入为什么堵不住
2.1 SQL注入:从一个登录框开始的噩梦
SQL注入的原理我尽量用大白话讲:你的程序本来打算执行一条固定的SQL,但因为把用户输入直接拼进了SQL字符串,用户输入的内容里带了特殊的引号或注释符,就把你的SQL语句"截胡"了。经典的' or '1'='1之所以能通杀很多登录接口,就是因为它把原本的密码校验逻辑变成了"永真条件"。
在我处理过的真实案例里,SQL注入的危害通常分三个层次:
- 绕过认证:不需要密码直接登录后台,这是最低级的利用方式。
- 拖库:用
UNION SELECT把其他表的数据查出来,用户名、手机号、密码哈希统统被人拿走。 - 写马提权:如果数据库账号权限够大,可以通过
INTO OUTFILE往服务器写文件,直接拿Webshell。
有个误区我必须强调:用了ORM框架不等于免疫SQL注入。我在排查一个Java项目的时候,发现团队用了MyBatis,但SQL语句是这么写的:
<select id="findUser" resultType="User"> SELECT * FROM users WHERE name = '${name}' </select>${name}是字符串拼接,相当于裸奔。正确写法应该是#{name}走预编译。所以就算你用了框架,也一定要做一次全局搜索,排查有没有${}拼SQL的地方。
2.2 命令注入:系统调用里最容易翻车
命令注入跟SQL注入思路一样,不同的是它往操作系统命令里拼东西。最常见的场景是后台管理工具里的"网络检测"功能,让你填一个IP或域名去ping,然后代码这么干:
String cmd = "ping -c 1 " + ip; Runtime.getRuntime().exec(cmd);这时候如果用户填的是127.0.0.1; cat /etc/passwd,分号后面那条命令就会被当作新命令执行。还有一种隐蔽的注入方式是利用反引号或者$(command),在不知不觉中执行任意命令。
防御上就两条硬规矩:
- 能用API绝不用命令行拼。比如Java里判断网络通不通,用
InetAddress.isReachable()替代去调系统ping命令。 - 必须执行外部命令时,用参数数组方式调用。不要拼完整命令行字符串,而是把每一个参数作为独立数组元素传给
exec,这样即使参数里带特殊字符,也只是被当作文本,不会被Shell解释。
2.3 注入类防御的统一打法
很多人只盯着加过滤函数,比如转义单引号、过滤分号,这种思路防不住变了形的payload。我比较推荐的是"三层防线的纵深配合":
- 第一层:参数化查询/预编译。这是SQL注入的根治方案,让SQL语句结构在执行前就固定下来,用户输入永远只当数据看。
- 第二层:输入校验。能明确格式的字段就做白名单校验,比如IP就只允许数字和点,邮箱就只允许邮箱正则。
- 第三层:数据库最小权限。业务账号只用
SELECT/INSERT/UPDATE/DELETE,坚决不给FILE权限,至少数据库被攻破时,拖库的难度会大很多。
提示:对用户输入的过滤要放在服务端做,前端校验只是用户体验,不是安全防线。
3. 前端对抗的攻防战:XSS与CSRF的相爱相杀
3.1 XSS:不直接攻击你,而是控制你的浏览器
跨站脚本攻击(XSS)的本质是把攻击者的脚本注入到你的页面里,让它在你浏览器的域下执行。为什么可怕?因为脚本在你浏览器里跑,相当于攻击者借了你的手操作你的账号——发微博、改密码、转账,全都能代劳。
我按触发方式把XSS分成三类,实际开发中都要考虑:
- 反射型:脚本藏在URL参数里,比如
?keyword=<script>alert(1)</script>,服务端没做转义直接回显到页面。这种攻击要骗用户点击恶意链接才会触发,通常配合钓鱼邮件。 - 存储型:脚本被存到数据库里。比如在评论区提交
<script>alert(1)</script>,所有打开这个页面的用户都会中招,危害更大。 - DOM型:纯前端漏洞,服务端根本没参与。比如JS用
innerHTML直接渲染URL参数里的内容,前端代码里躺着雷。
防御XSS,最核心的是分上下文编码。这句话我强调多少遍都不为过:在HTML标签里输出变量就用HTML实体编码,在JS字符串里输出变量就用JavaScript转义,在style属性里就做CSS转义。一套编码通吃所有场景是不存在的。
另外两个兜底手段很有效:
- 设置CSP(Content Security Policy)响应头,比如
Content-Security-Policy: default-src 'self',告诉浏览器只加载本域名的脚本,即使攻击者注入了脚本标签,浏览器也会拒绝执行。 - Cookie 加
HttpOnly标志,这样JavaScript读不到会话Cookie,就算XSS成功了,攻击者也没法直接偷走登录态。
3.2 CSRF:浏览器替你出手的"影子操作"
CSRF(跨站请求伪造)的思路完全反过来,攻击者不碰你的脚本,而是骗你的浏览器向目标网站发请求。原理是浏览器在发请求时会自动带上目标域名的Cookie,服务器分不清这个请求是用户主动发的,还是恶意页面诱导的。
举个真实场景:你登录了某论坛,然后在另一个标签页打开了一个带恶意图片的网站:
<img src="https://forum.example.com/post?type=delete&id=10086" />只要你的论坛登录态没过期,这个请求发出去,帖子就被删了。整个过程你完全无感。
防御CSRF,现代框架基本都有内置方案,但很多老项目还是漏的。我建议按三层来加固:
- CSRF Token:服务端在渲染表单时生成一个随机Token存到session,提交时校验。攻击者无法跨域读取你页面的Token,所以伪造不了合法请求。
- SameSite Cookie:给Cookie加
SameSite=Lax或Strict,浏览器会限制跨站请求携带Cookie,这是成本最低的一招。 - 校验自定义请求头:如果接口只接受
X-Requested-With: XMLHttpRequest,攻击者的跨域简单请求就发不出来,因为自定义Header会触发浏览器的CORS预检,而跨域预检通常过不了。
3.3 前端对抗里最容易漏的细节
这里说两个我在代码评审里反复提醒的点。
第一个是富文本编辑器。很多团队意识到要过滤用户内容,于是对评论区做了转义,结果产品要求支持富文本,就把HTML标签白名单放开了一部分。问题出在onerror、onclick这类事件属性上,如果只过滤了<script>标签而忘了事件属性,攻击者用一个<img src="x" onerror="alert(1)">就能绕过。处理富文本最靠谱的方式是用成熟的白名单库,比如Java的OWASP Java HTML Sanitizer,把标签和属性都限定死。
第二个是接口是否需要校验Referer。CSRF的Token方案也有一个坑:如果Token是写在URL参数里,Referer会泄露Token;如果Token放在请求头里,就必须用AJAX,而AJAX跨域又是个坎。所以我个人更推荐把Token放在自定义Header里,然后对非AJAX请求直接拒绝,算是双保险。
4. 信任链上的两个大洞:SSRF与文件上传
4.1 SSRF:让服务器替你访问内网
服务端请求伪造(SSRF)属于这几年曝光率飙升的攻击类型。原因是云服务普及之后,内网资源越来越有"含金量",而业务里经常有这样的功能:用户提交一个URL,服务器去抓取这个URL的内容再返回给用户。比如在线图片代理、PDF预览、Webhook回调测试、部分SSO配置。
攻击者一旦发现这个功能,会把URL改成内网地址去探测:
http://127.0.0.1:8080探测本机端口http://192.168.1.1/扫描内网网段file:///etc/passwd直接读本地文件(如果后端支持file协议)
我在一次攻防演练中遇到过最狠的利用:目标是某个云主机上的应用,攻击者通过SSRF访问云平台的元数据地址,拿到了临时访问凭证,结果整个对象存储的读权限都丢了。所以SSRF的防御绝对不能只靠"过滤用户提交的URL",要按下面这套思路来:
- 协议白名单:只允许
http/https,禁用file、dict、gopher等协议。 - IP黑名单(重定向也要查):解析URL拿到IP后,判断是否为内网IP、保留IP、云元数据IP,是就拒绝。注意必须对重定向后的新URL再检查一遍,否则很容易用
http://example.com/redirect?target=内网地址绕过。 - 域名解析校验:防止DNS重绑定攻击,就是在解析域名和建立连接之间,IP被悄悄换了。稳妥的做法是自行解析域名并校验IP后,再用这个IP去连接,而不是让系统去解析。
4.2 文件上传:从图片马到Webshell的经典路径
文件上传漏洞之所以危险,是因为它往往直接通向代码执行。攻击思路通常是这样的:
- 上传一个包含恶意代码的PHP/JSP文件,扩展名改成
.jpg或.png。 - 如果服务器配置不当,比如Apache对某些扩展名的解析顺序有问题,或者Nginx配置了
php_engine off但允许某些路径直接执行,这个"图片"可能被当作脚本执行。 - 如果访问不了,就尝试找文件包含漏洞,把上传的图片内容包含进去执行。
我之前排查过一个出问题的项目,对方做了文件类型校验,但只检查了HTTP头里的Content-Type。攻击者在Burp Suite里手改一下请求头,Content-Type从image/png换成application/x-php,直接就上传成功了。
正确做法我建议一套组合拳:
- 魔数校验(文件头校验):用程序读取文件开头的几个字节,比如JPEG的
FF D8 FF,PNG的89 50 4E 47,跟扩展名匹配才放行。 - 随机化存储文件名:上传后重新命名为
UUID.jpg,用户永远没法控制实际文件路径和扩展名。 - 存储与执行分离:上传的文件存到独立的文件服务器或对象存储,并设置为不支持脚本执行,就算文件内容有问题,最多也就是个静态图片。
- 图片重新编码:如果业务只需要图片,可以对上传的图片做一次重压缩/重采样,这个操作会破坏图片里嵌入的恶意代码,堪称"杀图片马"神器。
4.3 这两类攻击其实都是边界问题
聊到这里你会发现一个共性:SSRF是让服务器"主动"访问了不该访问的地方,文件上传是让服务器"被动"接收了不该接收的文件。两者本质上都是边界信任被突破了。
我在设计系统架构时,习惯把这两类攻击的防御放在网关层统一处理:对外暴露的文件上传接口统一走对象存储,对外暴露的URL抓取功能统一走代理服务,并且代理服务部署在独立的内网区域,只开放必要的出站白名单。这样就算业务代码写得再粗糙,边界上还能兜住一层。
5. 藏在对象流里的定时炸弹:反序列化攻击
5.1 为什么反序列化会成为高危项
反序列化攻击属于原理稍微难理解、但一旦被打通就是直接RCE(远程代码执行)的高危漏洞。我用大白话解释:序列化是把对象变成字节流方便存储或传输,反序列化就是把字节流还原成对象。问题出在——还原的过程中,如果数据本身是攻击者构造的,就可能触发对象生命周期里的某些魔法方法,这些方法里如果存在危险操作,就被攻击者利用上了。
最典型的场景是Java配合Commons Collections等常用库,攻击者构造一个特殊的序列化流,当服务端调用ObjectInputStream.readObject()时,利用库内部的反射链,一步步达到执行任意命令的目的。PHP也有类似问题,unserialize()配合魔术方**法__destruct、__wakeup等,同样能打出命令执行。
我接触过的真实案例里,常见入口有这么几个:
- 没加鉴权的Dubbo/Feign接口直接传序列化对象
- 登录态用Java序列化对象存Cookie
- 消息队列里传Java对象
- 某些老旧的Session存储方式
5.2 反序列化防御的实操方案
防反序列化攻击,思路跟SQL注入的参数化很像,核心是让不可信数据永远不要进入反序列化流程。具体落地建议:
- 优先用JSON等纯数据格式。能传字符串就传字符串,别为了省事直接传对象。大部分业务场景,用JSON序列化传输足够,完全不碰
readObject。 - 加签名认证。如果你的系统确实需要反序列化对象,在序列化之前加一个HMAC签名,反序列化之前校验签名,保证数据只可能来自你自己的服务端。这样攻击者虽然能看到序列化内容,但没法篡改。
- 类白名单过滤。使用
ObjectInputFilter(JDK 9+)或ObjectInputStream的重写,只允许反序列化项目里已知的类,其他类直接拒绝。
提示:反序列化漏洞很难靠WAF拦截,因为攻击流量看起来就是一段二进制乱码。所以运行时防护(RASP)和类过滤这类"内防"手段更重要。
5.3 我见过的最隐蔽的反序列化入口
最难查的反序列化漏洞不是那种明显传对象的接口,而是隐藏在第三方组件里的。比如某次排查,日志里一直看不到异常请求,但服务器被种了Webshell。最后定位到一个老版本Apache Shiro框架,它默认用Java序列化来加密和管理Session,攻击者只需要拿到加密密钥(硬编码在开源版本里),就能构造恶意序列化数据直接打穿。这类问题纯粹属于供应链层面的漏洞,除了及时升级组件版本、关注依赖安全通告,定期做依赖SCA扫描是唯一有效的办法。
6. 资源层面的持久战:DDoS、CC与慢速HTTP
6.1 从流量型DDoS到应用层CC
我先区分一组概念:
- DDoS(分布式拒绝服务):通常指流量型攻击,用大量僵尸主机向目标服务器发送海量数据包,把带宽或连接数打满。常见的UDP Flood、SYN Flood、ICMP Flood都属于这一类。
- CC攻击(Challenge Collapsar):属于应用层攻击。攻击者不断请求某个消耗资源的业务接口,比如搜索、导出、登录,用大量假请求把应用服务器的CPU或数据库连接池占满。
- 慢速HTTP攻击:最阴险的一种,攻击者建立连接后长时间不发送完整数据,把服务器的并发连接数一点点耗尽,比如Slowloris就是发一部分HTTP头然后一直吊着。
我处理过一个比较典型的CC攻击:某活动页面被刷了几百万次验证码请求,实际上攻击者并不需要登录,就是在不停地请求下发短信的接口。因为接口没有做频率限制,导致短信通道被刷爆,服务器也跟着慢下来。这种攻击不靠流量也不难识别,但特别消耗运维精力。
6.2 资源层防御的层次化思路
对DDoS这类攻击,防御没法靠应用代码自己搞定,要分层次布局:
| 防御层次 | 手段 | 应对攻击类型 |
|---|---|---|
| 网络层 | 云服务商的DDoS高防、流量清洗 | 大流量型DDoS |
| 接入层 | CDN隐藏源站IP、负载均衡限制连接数 | 基础攻击、端口扫描 |
| 应用层 | WAF拦截恶意特征、接口限流 | CC攻击、慢速HTTP |
| 业务层 | 验证码、频控、用户行为分析 | 针对核心接口的CC |
特别提醒一点:CDN隐藏源站IP是应用层防御的基石。如果源站IP泄露了,攻击者直接打源站,再多的高防也是空转。泄露源站IP的常见渠道包括:DNS历史记录、子域名的A记录、SSL证书日志、邮件头的Received字段,这些都要定期查一遍。
6.3 慢速HTTP攻击的排查与兜底
慢速HTTP攻击最难防,是因为它看起来像正常用户。排查的时候可以关注这几个指标:
- 服务器连接数持续走高,但CPU、带宽都正常。
- 大量连接的请求头不完整,几十秒都没发完。
- 同一IP的连接数异常多,User-Agent千篇一律或故意模仿搜索引擎。
防御手段上,Web服务器层面可以做三件事:设置请求头超时时间(比如Nginx的client_header_timeout)、设置最大请求体大小(client_max_body_size)、限制单IP最大连接数(limit_conn)。但这只是基础配置,真正对抗慢速HTTP最好在四层负载均衡层限流,或者使用能识别"慢速连接"的WAF。
7. 古老但从未缺席的隐患:路径遍历与XXE
7.1 路径遍历:看起来很简单,绕起来千变万化
路径遍历漏洞的原理一句话:用户提交一个文件名,程序没做检查就去拼接路径读取了。最常见的攻击方式:
GET /download?filename=../../../../etc/passwd如果后端直接拼接文件路径去读文件,攻击者就能一路往上跳目录,把系统敏感文件读出来。
你可能觉得现在不会有这么傻的代码了,但实际上我见过加了过滤还照样被绕过的:
- 过滤了
../,但攻击者用....//,因为过滤后剩下的../刚好能被解析。 - 过滤了点号,攻击者用URL编码
%2e%2e%2f,服务端如果先解码再拼接路径,正好还原。 - 用Windows路径的
..\,在支持Windows的服务器上也能生效。
防御路径遍历,业界一致认可的方法是规范化后校验前缀。具体做法是:用户提交的文件名先做一次路径规范化(比如Java的Paths.get(path).normalize()),然后判断规范化后的完整路径是否仍然在允许的根目录内,不在就拒绝。同时,拼接文件路径时不要直接用用户输入,而是用文件ID去数据库里查预定义的存储路径。
7.2 XXE:老旧XML解析器里的老熟人
XXE(XML外部实体注入)在前几年爆发过一阵,现在很多现代框架默认禁用了外部实体,但老系统里依然是高发漏洞。它的攻击原理是:XML解析器遇到DOCTYPE声明的外部实体时,会把实体指定的文件内容替换进来。攻击者构造一个这样的XML:
<?xml version="1.0"?> <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]> <foo>&xxe;</foo>服务端解析完这个XML后,返回值里就带上了/etc/passwd的内容。更高级的玩法是盲打带外数据,实体指向攻击者控制的服务器,通过请求日志读取目标内网文件内容,绕过了回显限制。
XXE防御最简单有效的手段就是在解析XML之前禁用外部实体。Java的DocumentBuilderFactory要设置setFeature("http://apache.org/xml/features/disallow-doctype-decl", true),PHP要用libxml_disable_entity_loader(true)。如果业务确实需要解析用户上传的XML,建议先做分析,看能否用JSON替代。
7.3 这两类漏洞的共同启示
路径遍历和XXE,本质上都是用户输入被当成了路径或结构的一部分,只是载体不同(文件路径 vs XML结构)。在代码评审的时候,我会重点检查所有接收文件路径、URL、XML/JSON数据的地方,问三个问题:这个输入能到达什么位置?它会被用来拼接什么?解析逻辑里有没有放行"外部引用"的开关?这三个问题问完,大部分配置层面的漏洞就藏不住了。
8. 统一防线:纵深防御与自主排查清单
8.1 安全的本质是纵深,不是单点铜墙铁壁
聊完十种攻击,我想强调一个观点:没有任何一种防御能单独挡住所有攻击。WAF会被绕过,参数校验总有漏网之鱼,框架总有未升级的版本。所以我才一直强调"纵深防御"——每一层都拦截一部分风险,即使某一层被突破,下一层还能兜住,最终为应急响应争取时间。
一个相对完整的Web纵深防线可以这样设计:
- 边界层:CDN、WAF、DDoS高防,拦截大部分流量型攻击和自动化工具有特征的行为。
- 接入层:网关统一做鉴权、限流、参数校验、异常检测。
- 应用层:代码层面落实参数化查询、输出编码、白名单校验、安全头。
- 数据层:数据库最小权限、敏感字段加密、审计日志。
- 运行时层:RASP、依赖SCA扫描、容器基线检测。
8.2 一张可以照着做自查的清单
每次有新项目上线,或者老项目做安全整改,我会把下面这张表拉出来当checklist过一遍:
| 检查项 | 自查问题 | 对应攻击 |
|---|---|---|
| SQL注入 | 是否有${}拼接SQL?所有查询是否预编译? | SQL注入 |
| 命令执行 | 是否有Runtime.exec或shell_exec?参数可控? | 命令注入 |
| 输出编码 | 动态内容按上下文做了编码吗?CSP头配置了吗? | XSS |
| 会话安全 | Cookie加了HttpOnly和SameSite吗?CSRF Token覆盖了吗? | CSRF |
| 文件上传 | 魔数校验了吗?存储和解析分离了吗? | 文件上传 |
| URL请求 | 服务器发起的请求有限制协议和网段吗? | SSRF |
| 序列化 | 反序列化接口有类白名单或签名吗? | 反序列化 |
| 频率控制 | 核心接口有限流吗?连接数有上限吗? | DDoS/CC |
| 路径读取 | 所有文件读取都校验了规范化路径吗? | 路径遍历 |
| XML解析 | 解析XML时禁用外部实体了吗? | XXE |
8.3 安全巡检不是一次性的差事
最后说点经验层面的东西。我见过太多团队,上线前搞一次安全测试,修完就以为高枕无忧了。但Web系统的攻击面是不断变化的——新功能上线、第三方组件升级、配置被调整,都可能引入新的漏洞。所以我的习惯是把安全巡检嵌入到CI/CD流水线里,每次构建自动跑一遍依赖SCA扫描、常见漏洞端口检查、安全检查项扫描,有高危问题直接阻断发布。这比定期人工渗透测试更贴合实际,因为很多漏洞是在开发过程中就埋进去的,越早发现修复成本越低。
另外,告警日志一定要有人看。WAF、Web服务器、应用日志里的异常告警,如果只是躺在监控系统里没人处理,那约等于没装。可以按严重级别做分级响应:注入类的告警必须立刻人工排查,扫描类的告警可以按天汇总分析,恶意IP自动拉黑。这样安全运维才不会变成"装了个寂寞"。
我自己踩过一个坑,在这里也提醒大家:某次上线前把所有"高危漏洞"都修了,但用的是那种教条式的修复——比如一遇到XSS就加个全局编码过滤器,一遇到SQL注入就统一在控制器入口做转义。结果呢?编码过滤器把富文本标签也全编码了,业务功能直接废了;SQL转义在预编译语句面前多此一举,还引入了字符集相关的乱码问题。所以我对安全的整体态度是:理解漏洞的根本成因,在正确的层做正确的防御,而不是看见漏洞的名称就往上堆过滤器。这也是我写这份总结的初衷——希望看完之后,你能对每一种攻击的"为什么"有真正的理解,而不是只会搜"XX攻击怎么防御"然后复制粘贴。