《网络安全的攻防启示录》· 第一篇章:破壁之术 · 第4篇
堡垒往往从内部被攻破,而Web应用最大的威胁,就藏在它对待“自己人”的方式里。
想象一下这个场景:你在一个电商网站下单后,在浏览器地址栏里看到这样一个链接:
https://www.example.com/order.php?order_id=12345
你出于好奇,或者不小心手滑,把 12345 改成了 12346 ,按下回车。令人惊讶的是,屏幕上竟然显示了另一个用户的完整订单信息——姓名、电话、地址、购买商品一览无余。

越权访问漏洞(水平越权)场景示意图
图注: 直观展示攻击者仅通过修改ID就访问到了不属于自己的数据。
这不是虚构的故事,而是每天都在互联网上真实发生的越权访问(Broken Access Control)漏洞。更令人担忧的是,这仅仅是Web应用“内患”的冰山一角。
作为在软件行业深耕30多年的从业者,我见证了太多这样的案例。有一次在为某金融机构做架构咨询时,他们的开发团队负责人自信地告诉我:“我们系统很安全,用了最新防火墙、做了加密传输。”但在后续的代码抽查中,我们发现了一个简单的SQL拼接操作——一个可能导致整个客户数据库被泄露的“小疏忽”。
真正的安全威胁,往往不在外部的高墙之外,而在内部那些被我们视为“理所当然”的代码逻辑中。
今天,我们就来深入剖析Web应用的三大“内患”:注入(Injection)、越权(Broken Access Control)与命令执行(Command Execution)。
这些漏洞之所以如此普遍且危险,不是 因为它们的技术有多么高深莫测,而是 因为它们直接利用了应用系统对“自己人”——也就是正常用户、正常输入和正常业务流程——的过度信任。
一、 信任的崩塌:为什么这些“内患”如此普遍?
在深入具体漏洞之前,我们必须先探讨一个根本问题:为什么这些早在20年前就被反复提及的基础漏洞,在2024年的今天仍然大面积存在?
思考小札:
在我早期的项目管理经历中,团队最关心的指标是“功能是否按时实现”(Time to Market)和“性能是否达标”。安全,往往是排在最后,甚至被牺牲掉的那个。
我常常在评审会上听到这样的声音:“这个功能先上,安全问题后面再迭代优化。”“这是内网系统,没必要搞这么复杂。”这种“功能优先”、“内部可信”的开发文化,正是滋生这些“内患”漏洞的完美温床。
1.1 开发者的“信任预设”
大多数开发者在编码时,心中都有一个“理想用户”模型:用户会按照我预期的方式使用功能。
- 当用户输入用户名时,他输入的就是用户名(zhangsan);
- 当用户访问订单页面时,他只会看自己的订单(order_id=12345)。
这种预设本身没问题,问题在于,代码逻辑完全建立在了这个“善意”预设之上,没有任何防范“恶意”的措施。
1.2 复杂性的“保护色”
现代Web开发框架(如Spring Boot, Django, Ruby on Rails)和ORM(对象关系映射,如MyBatis, Hibernate)的复杂性,反而给这些基础问题提供了“保护色”。
我们很容易产生一种错误的安全感——“框架已经帮我处理了安全问题”。
但真相是:框架能提供保护,但不能替代开发者的安全意识。如果你错误地使用了框架(比如在ORM中仍然拼接字符串),或者在框架之外(比如直接调用系统命令)进行了危险操作,漏洞依然会产生。
这不是 框架没用,而是 我们误解了框架的责任边界,把本该自己承担的“验证”责任,甩给了框架。
二、 SQL注入(SQL Injection):当用户“篡改”了数据库指令
SQL注入是最经典、最具破坏力的“内患”之一。
它的原理: 攻击者通过构造特殊的输入,欺骗应用程序,使其将恶意代码作为合法的SQL查询语句的一部分,提交给数据库执行。
核心问题: 程序未能区分“数据”和“代码”。
2.1 案例:经典的登录绕过
一个极其不安全的登录验证SQL可能是这样拼接的:
Java
// 错误示范:通过字符串拼接构造SQL
String sql = "SELECT * FROM users WHERE username = '" + userName +
"' AND password = '" + password + "'";
// ... 执行查询 ...
- 正常用户: 输入 zhangsan 和 123456。
- 执行的SQL:SELECT * FROM users WHERE username = 'zhangsan' AND password = '123456'
- 攻击者: 在用户名框输入 admin' -- (注意:admin、单引号、空格、两个减号、空格),密码任意。
- 执行的SQL:SELECT * FROM users WHERE username = 'admin' -- ' AND password = '...'
在SQL语法中,-- (双减号) 是行注释符。这意味着,从--开始到行尾的所有内容(包括密码验证)都被数据库忽略了!查询语句瞬间变成了:
SELECT * FROM users WHERE username = 'admin'
如果admin用户存在,攻击者就无需密码,直接登录了系统。
2.2 演进:从“拖库”到“拿Shell”
SQL注入不是 只能绕过登录。它早已演化出更高级的形态:
- 联合查询注入 (UNION Attack): 利用UNION关键字,获取其他表的数据,比如整个users表或products表,这就是常说的“拖库”。
- 盲注 (Blind SQLi): 当页面不返回错误信息时,攻击者通过构造逻辑判断(布尔盲注)或利用时间延迟(时间盲注),像猜谜一样一点点“猜”出数据库里的内容。
- 堆叠查询 (Stacked Queries): 允许攻击者在一条查询中执行多条SQL语句(用分号;隔开),实现更复杂的攻击,如修改数据、删除表格,甚至在特定数据库(如SQL Server的xp_cmdshell)上执行操作系统命令(“拿Shell”)。

SQL注入攻击原理示意图
图注: 展示了攻击者的恶意输入(红色部分)如何污染了原始的SQL查询语义,导致逻辑判断失效。
2.3 防御启示:信任的“隔离舱”
如何防御SQL注入?不是 靠“黑名单”过滤(比如禁止'或--),因为攻击者总能找到绕过的方法(如编码)。
唯一的、根本性的解决方案是:参数化查询 (Parameterized Queries),也叫预编译 (Prepared Statements)。
Java
// 正确示范:使用参数化查询
// 1. SQL语句使用 '?' 作为占位符
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
// 2. 预编译SQL(数据库已"理解"了SQL的结构)
PreparedStatement stmt = connection.prepareStatement(sql);
// 3. 传入参数(此时传入的内容只会被当作"数据",绝不会被当作"代码")
stmt.setString(1, userName);
stmt.setString(2, password);
// ... 执行查询 ...
这种方式,就像是为用户输入的数据准备了一个“隔离舱”。无论攻击者输入' OR '1'='1 还是别的什么,数据库都只会把它当作一个普通字符串(数据)去匹配,而绝不会将其作为SQL指令(代码)来执行。
核心启示: 永远不要信任用户输入,必须在代码层面将“数据”与“指令”彻底分离。
三、 越权漏洞:看不见的“权限边界”
如果说SQL注入是“骗过”了系统,那么越权漏洞就是“绕过”了系统。系统本身可能在正常工作,只是用户在自己合法的身份下,访问到了本不该属于他的资源。
核心问题: 系统验证了“你是谁”(认证),却没验证“你能干什么”(授权),或者验证得不彻底。
3.1 水平越权 vs. 垂直越权
权限攻击可以分为水平权限攻击和垂直权限攻击,越权主要分两种:
- 水平越权 (Horizontal Privilege Escalation):
- 定义: 攻击者访问与自己同级别权限的其他用户的资源。
- 场景: 正如开篇的例子,用户A通过修改URL中的order_id,看到了用户B的订单。
- 垂直越权 (Vertical Privilege Escalation):
- 定义: 攻击者(如普通用户)执行了更高级别权限(如管理员)的操作。
- 场景: 用户A只是普通用户,但他通过直接访问URL .../admin/delete_user.php,竟然成功打开了管理员才能访问的删除用户页面。
3.2 越权的根源:相信“前端会守规矩”
越权漏洞的产生,往往源于一个致命的信任预设:信任前端(或客户端)会“守规矩”。
思考小札:
在我们评审某保险公司的理赔系统时,发现了一个经典问题:普通理赔员的界面上“没有”“审批通过”这个按钮(前端代码将其隐藏了)。但如果理赔员通过抓包工具,直接伪造一个包含“审批”动作的请求发送到后端API,系统居然就通过了!
为什么?因为后端API的开发者默认只有管理员才能看到并点击那个按钮,所以他干脆就没在后端API上加权限验证。
这就是典型的“依赖前端控制权限”导致的垂直越权。攻击者不是 攻破了系统,而是 简单地绕过了你“隐藏”起来的门。
3.3 防御启示:以“会话”为准,而非“请求”
防御越权漏洞的核心原则:后端必须对所有资源的每一次访问,都进行严格的权限校验。
1、绝不信任用户请求中的ID:
Java
// 错误示范:信任请求中的ID
String orderId = request.getParameter("order_id");
Order order = getOrderById(orderId); // 此时可能获取了别人的订单
// 正确示范:从会话中获取用户身份,并校验资源归属
String currentUserId = session.getUserId(); // 1. 从会话获取当前登录用户ID
Order order = getOrderById(orderId);
// 2. 关键校验:检查订单是否属于当前用户
if (order.getUserId().equals(currentUserId)) {
// ... 允许访问 ...
} else {
// ... 拒绝访问(越权)...
}
2、统一的权限拦截: 将权限校验逻辑做成统一的拦截器(Interceptor)或过滤器(Filter),在所有敏感API执行前自动运行,而不是在每个业务方法里手动写一遍,避免遗漏。
核心启示: 用户的认证状态(登录了)和授权状态(能干啥)是两回事。必须在服务端,以“当前登录的身份”(Session/Token)为唯一信任源,去校验其对“目标资源”的“操作权限”。
四、 命令执行(RCE):从Web应用到操作系统的“致命跳跃”
这是Web“内患”中最危险的一种,因为它允许攻击者直接从Web应用“跨界”到服务器操作系统,执行任意命令。
核心问题: 应用程序将用户输入作为操作系统命令的一部分来执行。
4.1 场景:危险的“便捷功能”
很多Web应用确实需要执行系统命令,比如:
- 调用 ping 命令测试网络连通性。
- 调用 ImageMagick 库处理用户上传的图片(如缩放、裁剪)。
- 调用系统脚本执行备份或数据处理。
问题不是 “执行命令”,而是 “拼接”命令。
4.2 案例:图片处理的“分号”惨案
假设一个图片处理功能,后端代码如下:
Java
// 错误示范:拼接系统命令
String command = "convert " + userUploadedImagePath + " -resize 800x600 output.jpg";
Runtime.getRuntime().exec(command);
- 正常用户: 上传路径 '/tmp/image.jpg'
- 执行命令:convert /tmp/image.jpg -resize 800x600 output.jpg (功能正常)
- 攻击者: 构造一个恶意路径 '; rm -rf / ;'
- 执行命令:convert '; rm -rf / ;' -resize 800x600 output.jpg
在Linux/Unix中,分号:是命令分隔符。这条命令被系统解析成了三条:
- convert ' ' (执行失败或空操作)
- rm -rf / ( 删库跑路! )
- ' ' -resize 800x600 output.jpg (执行失败)
攻击者通过一个简单的分号,实现了从应用层到系统层的“致命一跃”。
4.3 防御启示:隔离“参数”与“指令”
防御命令执行漏洞的思路,与防御SQL注入如出一辙:隔离数据与指令。
1、首选:避免调用系统命令。 尽量使用语言内置的库(如Java的图像处理库)来完成功能,而不是调用外部程序。
2、次选:使用参数数组(永不拼接)。
Java
// 正确示范:使用参数数组
// 指令和参数被严格分离
String[] command = {"convert", userUploadedImagePath, "-resize", "800x600", "output.jpg"};
Runtime.getRuntime().exec(command);
这样,即使用户输入 '; rm -rf / ;',它也只会被当作一个无效的文件名参数传递给convert命令,而绝不会被操作系统当作新的指令来执行。
3、最后:严格的白名单验证。 如果必须拼接(极不推荐),也要对输入进行白名单验证(例如,只允许字母、数字、下划线和点),而不是黑名单过滤(禁止;|&等)。

命令执行漏洞防御体系
图注: 展示了防御命令执行的理想流程,核心在于验证、安全构造和隔离。
五、 核心启示:安全编码的本质,是“信任管理”
当我们剖析完这三大“内患”,会发现它们都指向同一个根源:开发者在代码中赋予了不该有的“信任”。
- SQL注入: 信任用户输入的内容是“纯数据”,而不是“SQL指令”。
- 越权访问: 信任用户提交的标识符(如order_id)代表了他有权访问的资源。
- 命令执行: 信任用户输入的内容是“合法参数”,而不是“系统命令”。
因此,防御这些“内患”的统一心法,就是在代码层面践行“零信任”原则。
不是 问“这个输入安全吗?”,而是 问“我凭什么信任这个输入?”
- 永不信任输入(Input Validation): 视所有输入(URL参数、POST数据、HTTP头、Cookie...)为潜在威胁,进行严格的白名单验证和规范化。
- 分离代码与数据(Separation): 严禁使用任何形式的字符串拼接来构造查询或命令。必须使用参数化查询或参数数组。
- 最小权限(Least Privilege): Web应用连接数据库的账户,绝不能是root或sa,应是仅有增删改查权限的受限账户。执行系统调用的进程,应在容器或沙箱中以最低权限运行。
- 以服务端为准(Server-Side Authority): 绝不依赖前端(JavaScript、HTML隐藏)进行安全控制。所有权限判断必须在后端,基于可信的会话(Session)信息进行。
结语:从“亡羊补牢”到“未雨绸缪”
Web应用的这些“内患”,本质上都是系统内部的信任管理失当。
作为从业三十余年的老兵,我见过太多“亡羊补牢”的紧急修复——半夜上线补丁、回滚数据、安抚客户。也参与过一些“未雨绸缪”的成功实践——在设计阶段就引入威胁建模,在CI/CD流水线中集成安全扫描。
我可以负责任地说,两者的成本差异是指数级的。
真正的安全,不是 发现漏洞后再去修补,而是 在设计之初、在敲下第一行代码时,就建立起“不信任”的思维模式。
下次当你编写处理用户输入的代码时,不妨多问自己一句:“如果这个用户是恶意的,他会如何滥用我这个设计?”
这个简单的思维转变,或许就是你的应用从脆弱走向坚固的关键一步。
下篇预告:
了解了Web应用的“内患”后,我们将在下一篇探讨《第5篇 | Web应用的“外邪”:XSS、CSRF与SSRF漏洞详解》,看看攻击者如何从外部利用应用的正常功能(如浏览器、其他服务器)发起攻击。敬请期待。
互动时刻:
- 本篇小结: 我们深入剖析了Web应用的三大“内患”——SQL注入、越权访问和命令执行。我们发现,这些漏洞的根源不是 技术高深,而是 对用户输入的“过度信任”。防御的核心在于“零信任”编码:彻底分离数据与指令,并在服务端严格校验权限。
- 思考/讨论: 回顾您当前或最近参与的项目,是否也存在文中提到的(如SQL拼接、依赖前端做权限控制等)“信任预设”问题?您认为在团队中推动“安全左移”(在开发早期就介入安全)最大的阻力是什么?欢迎分享您的想法和计划。
- 术语小词典 (更新):
- SQL注入 (SQLi): 通过在输入中插入恶意SQL代码,改变数据库查询逻辑的攻击。
- 越权 (BAC - Broken Access Control): 用户访问了其本不该有权访问的资源或功能。
- 命令执行 (Command Execution): 攻击者通过Web应用在服务器上执行了非预期的操作系统命令。
- 参数化查询 (Parameterized Query): 一种安全编码实践,将SQL指令模板与数据参数分离发送给数据库,从根本上防止SQL注入。
- CI/CD (Continuous Integration/Continuous Deployment): 持续集成/持续部署,现代软件开发的自动化流程。
- SAST (Static Application Security Testing): 静态应用安全测试,通过扫描源代码来发现漏洞。

642

被折叠的 条评论
为什么被折叠?



