Web安全三大高危漏洞:SQL注入、路径遍历与越权访问的防御实践
聊Web应用安全现状我最大的感受是高危漏洞的榜单年年换但真正让安全团队头疼的还是那几类老面孔——Injection、File System Traversal、Broken Access Control。每次做代码审计和渗透测试只要把这三个方向认真过一遍都能至少捞出一两个中高危问题。这篇文章不打算重复翻译OWASP Top 10而是把这三类攻击的底层逻辑、典型场景、修复要点放在一起拆解结合我在项目里的实际操作整理成一份能直接用于自查和防御的笔记。适合还没系统接触Web安全的开发者、刚转行的安全工程师也适合正在做代码评审的团队负责人——看完你至少能知道哪些代码写法必须禁止哪些接口设计必须补校验。1. 现状拆解为什么“老漏洞”还是主流1.1 攻击面膨胀带来的“漏洞密度”问题先聊现状。现在一个普通的Web应用已经很少是简单的单体页面了前端一套后端微服务拆成十几个中间还有API网关、消息队列、对象存储、第三方回调。光是一个订单功能可能就涉及商品服务、支付服务、用户服务、日志服务四个模块。接口数量从几十个涨到几百上千个但安全和研发资源并没有等比例增加。这带来的直接后果是“漏洞密度”被稀释了。安全团队没法每个接口都人工测自动扫描器又只能覆盖语法层面的问题于是边缘接口成了重灾区。我做过一次某公司的渗透测试核心交易链路防护做得很完整SQL注入基本堵死但管理后台一个文件导出接口path参数直接拼接路径轻轻松松就读到配置文件。这类问题在单体时代反而不常见因为功能少、评审容易覆盖服务拆细之后每个团队都觉得自己那部分“只是内部调用”结果就是谁都没管。所以理解Web应用安全现状不能只看漏洞类型本身还要看应用架构变化带来的暴露面增长。攻击者不需要找到所有漏洞只需要找到一个没人管的边缘接口一条链路就能打穿。安全团队的核心工作之一就是把“没人管的接口”压缩到最少。1.2 框架兜底与业务逻辑失控的错位第二个现状是开发框架解决了大量“技术型漏洞”但业务逻辑漏洞反而越来越突出。以前写SQL注入是因为大家都在拼字符串现在用ORM框架直接拼接SQL的情况少了很多参数化查询成了默认行为。模板引擎也默认转义反射型XSS在主流框架里没那么容易触发。很多开发者因此产生了“框架安全了应用就安全了”的错觉。但框架不会替你判断业务权限。举个例子一个订单详情接口只要用户登录就能访问参数里带着订单号orderId。后端代码只做了“是否登录”的判断没有校验这个订单到底是不是当前用户的。结果就是任何登录用户改一下订单号就能看别人的收货地址和手机号。这个问题属于典型的Broken Access ControlOWASP把它排在漏洞榜单第一不是没有道理——它没法靠框架默认配置解决必须靠开发者在每个资源访问点显式实现授权逻辑。框架解决了“注入语法”问题却把“业务语义”的判断留给了开发者。业务语义又恰恰是最容易被忽略的部分因为接口在开发环境里测出来是正常的只有拿到真实用户身份去交叉访问时才会暴露风险。这个错位是越权漏洞泛滥的根本原因。1.3 从榜单看三类攻击为什么始终靠前如果你关注OWASP Top 10的版本变化会发现一个有意思的现象Injection从曾经的榜首滑落到第三Broken Access Control反而升到第一。这不代表注入消失了而是“被框架兜住了一部分”但只要出现新的解释器、新的拼装方式注入马上就会换个马甲回来。比如NoSQL注入、GraphQL注入、模板注入都属于注入家族的新形态。File System Traversal在公开统计里的数量不如前两者多但危害一点不小。它的核心是CWE-22路径遍历常见于文件下载、上传、导出报表、多语言文件加载等场景。很多系统的文件参数都是明文路径甚至直接把相对路径拼进文件系统调用里。这类漏洞一旦和上传功能组合攻击者可以往可执行目录写入脚本直接升级为远程命令执行。三类攻击的共同特点根源都是“没有充分信任边界”。应用在把用户输入交给解释器、文件系统、业务对象之前没有做合法性验证。Injections是不验证语法边界File System Traversal是不验证路径边界Broken Access Control是不验证权限边界。后面我会逐个拆开讲。2. Injection从一个小字符到一次大事故2.1 注入的本质语法边界被打破Injection类漏洞我习惯把它理解成“语法边界被打破”。应用程序本来想把用户输入当作数据处理但由于拼接姿势不对用户输入被解释器当成了指令的一部分。拿SQL注入举例服务端代码写的是String name request.getParameter(name); String sql SELECT * FROM user WHERE name name ; Statement st conn.createStatement(); ResultSet rs st.executeQuery(sql);如果name的值为testSQL就是正常的条件查询。可一旦输入变成 OR 11 --拼接出来的语句就变成了SELECT * FROM user WHERE name OR 11 --OR 11让where条件永远成立--把后面的单引号注释掉。这样原本只查一个用户的接口变成了返回全表数据。登录接口遇到这种输入往往直接绕过认证。我常用一个生活类比来解释你在一张提货单上写“备注帮我带两箱水”仓库管理员只看备注就把水搬给你但如果备注写的是“顺便把仓库门锁密码发到群里”而系统没有任何机制区分“备注”和“指令”那就会被执行。注入就是这个原理区别在于Web应用里的“指令”是数据库查询、shell命令、表达式等等。2.2 SQL注入的常见分类与实战还原从攻防视角看SQL注入可以分几种形态类型特点常见判定方法报错注入通过数据库报错信息回显数据输入单引号观察报错联合查询注入使用UNION合并结果集确认列数后构造UNION SELECT布尔盲注页面返回差异化判断真假逐字符比较ASCII值时间盲注通过延时判断条件真假使用sleep函数堆叠注入一次执行多条SQL语句分号分隔多语句报错注入最简单也最直观。比如参数后面加一个单引号页面直接抛出“MySQL语法错误”这类异常说明输入被代入SQL执行了。联合查询注入需要先猜列数用ORDER BY n逐步试探直到不报错的临界值。盲注则更依赖响应时间的差异比如if(condition,sleep(3),0)页面延迟说明条件为真。这里必须强调一点这些操作只能在授权测试环境中进行。实际风险场景里攻击者不需要理解每个原理工具会自动完成这些试探而防御者恰恰需要理解原理才能在代码里发现“疑似注入点”。我给开发做培训时会现场演示一次完整的报错注入和盲注判断目的不是教攻击而是让开发者记住一个直觉参数后面出现单引号报错绝不是什么“用户输入不规范”而是危险信号。2.3 参数化查询不是万能药很多人一听到SQL注入第一反应是“用参数化查询”。方向没错但参数化查询不是万能药这也是我在实际修复中反复遇到的问题。参数化查询解决的是“SQL语句结构固定只有参数值变化”的场景。一旦SQL的语法结构本身需要动态拼接参数化就无能为力了。常见坑位包括动态表名或字段名SELECT * FROM ${tableName}表名不能写成占位符ORDER BY 排序字段ORDER BY ${sortField}排序字段不能参数化IN 子句的数量动态变化需要动态生成 ? 占位符MyBatis 里误用${}${name}直接拼接#{name}才是预编译存储过程内部的动态SQL参数化外层并不能治好内部拼接举一个我见过很多次的MyBatis错误写法select idgetUser resultTypeUser SELECT * FROM user WHERE name ${name} /select${name}会被MyBatis直接做字符串替换相当于自己拼SQL。改成#{name}就安全多了select idgetUser resultTypeUser SELECT * FROM user WHERE name #{name} /select修复之后的SQL语句结构在预编译阶段就确定了输入值只作为参数传递不管用户输入什么内容都不会再改变SQL语义。对于ORDER BY这类确实无法参数化的部分我的建议是建立白名单映射前端传索引或枚举后端映射到固定的列名而不是把用户字符串直接拼进排序子句。2.4 命令注入与NoSQL注入的变体SQL注入只是Injection家族里最出名的一个现实里还有命令注入、NoSQL注入、模板注入原理都是一样的把不可信数据拼进了另一个解释器。命令注入的典型写法是Java里用Runtime.getRuntime().exec拼接系统命令或者Python里用os.system拼接参数。攻击者传入; id、| whoami、$(whoami)这类特殊字符就能让应用程序执行额外的系统命令。防御方式很明确尽量不调用系统命令必须调用时使用参数数组而不是拼接一条字符串并且对入参做严格白名单校验。NoSQL注入则是把脏数据拼进了JSON查询表达式。比如一个MongoDB登录接口如果后端直接解析前端传来的整个查询对象攻击者可以构造{username: {$ne: null}, password: {$ne: null}}让数据库认为用户名不为空、密码也不为空直接绕过登录。这提醒我们任何查询语法如果允许用户直接传递操作符都要警惕表达式注入。对于Injection家族我的总体建议是把“拼接”当成最危险的编码操作。任何用户输入一旦被拼进SQL、shell命令、表达式、模板字符串都要先问“这一步真的无法避免吗”。多问几次你会发现很多拼接本来就不该存在。2.5 注入攻击的检测与拦截思路检测注入不能只靠扫描器。我习惯从三个层面看代码审计层面全局搜索拼接SQL、${}、Runtime.exec、os.system、child_process.exec等危险调用点逐个评估输入是否可控。动态测试层面在授权环境下用扫描工具辅助但不能盲目信任工具。工具报“疑似注入”要用手工请求验证工具不报不代表代码没问题尤其要对业务逻辑型“半注入”保持敏感。流量监控层面在WAF和网关层面对明显的SQL关键字进行拦截但切记WAF只是第一道闸不是最终修复。很多团队喜欢给所有接口套一个“全局转义”过滤器比如把单引号替换成两个单引号。这种方案在特定SQL模式下有效但很容易被编码绕过、宽字节绕过。真正的修复必须回到“永远用参数化/白名单校验”这条根上。提示给修复方案加“迷之过滤”——比如把../替换成空字符串、把单引号转义一遍——是最容易埋雷的做法。过滤规则永远落后于绕过技巧核心思路应该是“不允许就不拼”。3. File System Traversal一条路径引发的连锁风险3.1 路径拼接的“想当然”File System Traversal国内常叫“目录遍历”“路径穿越”。它不是新东西但几乎每年都能见到。问题出在开发者默认“用户传入的路径是可控的、安全的”。比如下载文件功能常见写法是String path request.getParameter(path); File file new File(/var/data/files/ path);如果path是avatar.jpg程序去读一个公开目录下的文件看上去没问题。但攻击者把path改成../../etc/passwd拼接后的路径就变成了/var/data/files/../../etc/passwd操作系统在解析时“..”代表上一级目录最终指向了/etc/passwd。“..”其实不是唯一手段攻击者还会尝试用绝对路径、URL编码后的%2e%2e%2f、双重编码等绕过方式。这也是为什么简单替换“..”的修复方式不可靠——今天你替换了../明天攻击者可能用....//或编码变体绕过。我在审计里判断路径遍历风险只看一个点你构建文件路径时是否直接使用了未经验证的用户输入。如果是那就是高危别管它前面加了多少层目录。3.2 任意文件读取导致的敏感信息泄露路径遍历最常见的危害是任意文件读取。听起来只是“读文件”但实际能挖出来的东西很致命应用源代码读WEB-INF/classes下的class文件反编译后理解业务逻辑为下一步攻击铺路配置文件数据库账号、Redis密码、云厂商密钥、JWT签名密钥往往都写在配置文件里环境变量读取/proc/self/environ可能直接拿到运行时环境变量运维信息读取/etc/passwd、系统版本文件帮助提权我见过一个真实案例系统有个“导出报表”功能参数是文件名拼到服务器本地目录。攻击者先读配置拿到数据库连接串然后连上去发现数据库账户是高权限直接把整个用户表拖走了。整个过程没有任何注入就是一条路径遍历。所以评估这类漏洞不要只盯着“能不能读passwd”要看“读完这个文件之后攻击者还能做什么”。高危路径遍历的核心价值不在于“读文件”这个动作本身而在于它为后续攻击提供了跳板。3.3 从路径穿越到远程命令执行路径遍历不只是“读文件”和上传功能组合时危害会急剧放大。如果应用允许上传文件且文件名由用户控制攻击者可以传一个../shell.php或../../webroot/shell.php。如果服务器解析到了Web根目录这个文件就会被当作可执行脚本访问攻击者就拿到了一个WebShell等于远程命令执行。这种场景我称为“文件写入型路径穿越”杀伤力比读取更大。除了上传还有一些隐蔽场景日志文件名、缓存文件名、多语言文件路径、模板加载路径都可能踩中。修复思路是用户提交的文件名一律不信任。真实落地时我一般建议服务端用UUID重新生成文件名原始文件名只存数据库不参与路径构建文件保存到固定的落地目录目录本身不允许被用户参数改变如果是对象存储直接用对象的唯一键不要让业务层感知本地路径不要用黑名单过滤文件名而是用白名单限制扩展名并且服务端校验MIME类型在文件名这个环节“随机化白名单”是性价比最高的方案。因为你无法穷举攻击者会用哪些字符串但你可以让业务根本不依赖用户提供的字符串来定位文件。3.4 文件遍历的防御实践具体到代码一个比较稳的修复是“规范化前缀校验”。以Java为例String path request.getParameter(path); Path base Paths.get(/var/data/files).toRealPath(); Path target Paths.get(base.toString(), path).normalize(); if (!target.startsWith(base)) { throw new SecurityException(文件路径非法); } File result target.toFile();原理很简单先把基础目录解析成真实路径再把用户输入的path拼进去、做normalize把里面的..、.规约掉最后检查结果路径是否仍然以基础目录开头。如果拼完之后跑到了基础目录之外直接拒绝。Python版本也是同样思路用os.path.realpath再判断前缀。不过要注意realpath可能会解析软链接如果基础目录自身包含软链接要先把base也realpath一遍再比较。除了代码层部署层也要做纵深防御。比如Web容器不直接对公网暴露文件系统目录、配置文件不出现在Web根目录、敏感文件做权限管控。这样即使代码漏了一处攻击者也难以直接读取高价值文件。4. Broken Access Control越权是业务漏洞之王4.1 横向越权与纵向越权Broken Access Control直译是“访问控制失效”国内更习惯叫“越权”。它和注入、路径遍历不太一样问题几乎全部出在业务逻辑层。先区分两类典型越权。横向越权是指普通用户访问了另一个普通用户的资源比如A登录后能看B的订单纵向越权是指低权限用户使用了高权限功能比如普通员工能调管理员的删除接口。横向越权常见于业务数据接口纵向越权常见于后台管理功能。为什么越权这么普遍因为开发者经常默认“接口只被前端调用前端只显示当前用户的菜单”。现实是攻击者根本不走你家的前端页面他们直接拿工具改个HTTP请求就完事。前端隐藏按钮、页面不展示菜单这些都属于“客户端控制”对服务端没有任何约束力。我见过不少系统管理后台的入口按钮被隐藏了但后端接口没有任何角色校验攻击者直接请求一个管理API就能绕过前端限制。4.2 IDOR最常见也最隐蔽的越权IDORInsecure Direct Object Reference是越权里最经典的形态。核心问题在于接口直接使用数据库自增ID作为资源标识后端却只校验“登录状态”不校验“资源归属”。典型场景GET /api/order/1001 HTTP/1.1 Authorization: Bearer 用户A的Token攻击者把URL里的1001改成1002如果后端没有校验这个订单的owner是不是当前用户就会返回用户B的订单信息。信息越敏感危害越大通讯录、订单、发票、病历任何一个资源只要存在IDOR数据就会批量泄露。修复方式分为两层。第一层是把可枚举的资源ID换成不可预测的标识比如UUID第二层也是更核心的一层是在服务端显式校验资源归属GetMapping(/api/order/{orderId}) public Order getOrder(PathVariable String orderId) { Order order orderService.getById(orderId); if (!order.getOwnerId().equals(当前登录用户ID)) { throw new AccessDeniedException(无权访问该订单); } return order; }UUID只能提高“猜”的成本真正挡住越权的还是owner校验。很多团队只做了第一层以为ID不可枚举就安全了但用户如果本身就是资源相关方比如同一个客户的多个联系人之间UUID并不能防止越权。权限校验必须和人绑定而不是和随机性绑定。4.3 服务端信任带来的权限失控除了IDOR还有几类越权写法值得点名批评用请求参数直接传角色比如roleadmin后端不加校验直接信任只判断“是否登录”不判断“角色是否匹配”普通用户能调管理接口管理后台嵌在同一个Web应用里只靠前端路由隐藏没有后端权限拦截缓存/CDN覆盖导致权限数据串用用户A的会话信息被用户B读取多租户系统里租户ID从请求头读取后端没有校验租户隔离这些问题的共同根因是“服务端过度信任客户端输入”。攻击者可以任意修改HTTP请求可以删除Cookie、篡改Header、遍历ID服务端必须把所有输入都当成不可信数据来处理。我在做代码评审时遇到“只要加了权限注解就完事”的说法都会格外警惕。权限校验不是加个注解那么简单注解可能没生效、可能被绕过、可能只覆盖了部分接口。关键要看每个接口入口是否都经过统一鉴权中间件资源级授权是否在业务代码里显式完成。4.4 越权防御的落地清单针对越权问题我整理了一份可以贴进团队Wiki的落地清单设计原则接口默认拒绝显式放行别用黑名单用白名单统一鉴权登录态、角色、权限点都集中在网关或中间件处理业务代码不重复实现对象级授权每个资源访问函数都要校验当前用户与资源归属的关系功能级授权RBAC基于角色的访问控制管住“哪些角色能调哪些接口”细粒度控制租户隔离、字段级权限用ABAC基于属性的访问控制补齐测试要求每个角色都测一遍调用关系矩阵重点测“改了ID能不能看到别人的数据”越权防御没有一劳永逸的方案因为它和具体业务强相关。但“每个资源访问点都必须有归属校验”这条铁律是通用的。把它写进代码规范要比依赖一两个安全人员的自觉可靠得多。5. 三类攻击的组合利用与纵深防御5.1 攻击链复盘从文件读取到权限提升把前面的三类攻击放到一条攻击链里你会看到Web安全为什么要做纵深防御。假设一个典型系统攻击者先通过文件下载接口的路径遍历读取到应用配置文件拿到了数据库连接字符串和JWT签名密钥。因为数据库账户配置了最小权限不能直接提权攻击者转而用JWT密钥伪造管理员Token。伪造之后调用管理后台的删除接口结果发现接口只校验“是否是管理员Token”没有校验操作人的真实身份于是攻击者顺利删除了全量业务数据。这条链路上单个漏洞的危害都没到“直接沦陷”的程度但组合在一起就失控了。如果文件遍历被修掉攻击者拿不到配置如果JWT密钥不放在Web可读取范围内攻击者伪造不了Token如果管理接口做了二次校验即使Token泄露也无法直接删数据。所以安全不是“修完一个高危漏洞就结束”而是要把每一层的风险都压到可接受范围。攻击者需要从入口一路打通到核心数据防御者只需要在链路任何一个节点把门关上。5.2 纵深防御的层次我把纵深防御拆成几个阶段每个阶段都有自己的责任阶段动作对应三类攻击的拦截点需求设计威胁建模、数据流分析明确资源权限模型编码阶段安全编码规范、代码审计禁止拼接SQL、禁止信任文件名上线前SAST/DAST、人工渗透扫描注入点、路径参数、越权接口运行期WAF、RASP、日志监控拦截恶意请求、感知异常行为应急阶段漏洞响应、复盘改进修复后再回归同类问题这里特别想说一下编码阶段的重要性。安全测试和WAF更像是“事后补救”如果在编码阶段就统一消灭“字符串拼接SQL”“用户文件名直接拼路径”“接口不做归属校验”这三类写法绝大多数高危问题根本不会进入线上。我见过一些团队把安全预算几乎全砸在WAF和扫描器上但代码里依然遍地是拼接。设备堆得再多也填不上开发的坑。先解决编码规范再谈安全工具这个顺序不能颠倒。5.3 安全测试的核心抓手安全测试不可能穷尽所有漏洞所以要有抓手。我的习惯是先做资产梳理把域名、服务、接口、数据流转画出来标注哪些接口涉及敏感数据。然后按风险高低排序高风险的先测。自动化扫描适合做广撒网比如SQLMap扫参数注入、nuclei跑模板规则、AWVS走一遍常见漏洞。但业务逻辑漏洞必须靠人工登录后修改订单ID看会不会越权、上传带路径的文件名看会不会穿越、把普通用户Token换成管理员Token看会不会放行。自动化工具最怕的是“误报和漏报一样多”。一款工具报了几十个“中危”真正手工验证可能全是误报。我一般要求测试报告里附上完整的复现请求和响应不能只丢一句“存在注入风险”。修复方拿到复现包才能快速定位到具体代码。6. 常见问题排查与实操心得6.1 我踩过的坑做安全这么多年我自己也踩过不少坑挑几个写出来供参考。第一个坑是“只测主链路”。以前给某公司做测试盯着订单支付、用户登录这些核心流程扫了半天结果一个文件导出接口的参数没测。测试报告发出去后正式环境被人用路径遍历把数据库配置读走了。从那以后我检查范围一定会覆盖所有“带文件名的接口”和“管理类接口”哪怕这些接口在业务上看着不重要。第二个坑是“修复方案过度简单”。同事刚开始遇到路径遍历直接在代码里加了一句replace(../, )想着把点点点去掉就安全。结果攻击者用....//、URL编码一下就绕过了。后来改成规范化前缀校验才算真正封住。修复漏洞不能只针对“见过的payload”要针对“根因”。第三个坑是“权限校验复制粘贴时漏改”。同一个项目里订单接口都校验了归属但新增的发票接口复制代码时漏了那一行。这种问题代码审计很难一眼看出来必须靠自动化规则和对照测试。现在我看到接口代码里出现“当前用户ID”都会要求旁边必须有资源归属判断缺一不可。6.2 三类攻击的自查速查表给团队做自查时我通常发一张速查表按漏洞类型列出代码特征、建议修复和绕坑点漏洞类型危险代码特征核心修复常见误区InjectionSQL字符串拼接、${}、Runtime.exec参数化查询、白名单、命令参数数组只加过滤器、只部署WAFFile System Traversalnew File(base 用户输入)、文件名直传上传normalize前缀校验、UUID重命名简单替换“..”、黑名单后缀Broken Access Control接口只校验登录态、资源ID来自请求参数统一鉴权中间件对象归属校验前端隐藏按钮、UUID替代校验表格看起来简单但每条背后都是真实漏洞换来的教训。代码评审时照着这张表过至少能挡住一大半常见高危。我建议团队把这张表挂在CI平台首页每次提交代码之前开发自己先对照一遍。6.3 给团队的一条实在建议最后分享一个我自己的团队在推的做法把“安全修复”从救火变成流程。我们不满足于提交漏洞报告让开发改而是在CI流水线里接了SAST工具把“禁止字符串拼接SQL”“禁止直接使用用户输入构建文件路径”这类规则固化成规则集提交代码时自动扫描拦截高危模式。接口侧的越权问题暂时没法完全靠静态规则解决所以强制要求每个新接口必须通过“角色矩阵”评审并且在测试环境跑一遍越权用例。这三类攻击在未来很长一段时间还会继续存在。它们不是新技术而是根因没被消灭的旧问题。与其不停追着漏洞跑不如在编码阶段就统一堵住根因。这是我从大量实战里得到的最实在的一条经验——安全能力的提升靠的不是某一次专项治理而是把这些基础动作变成日常习惯。