越权分为三部分讲解:1. 未授权访问;2. 垂直越权;3. 水平越权
访问控制
常见的控制有基于角色的访问控制和基于资源的访问控制。
基于角色的访问控制以角色为主体,以角色进行访问控制粒度较粗,可扩展性不强,不利于系统维护。
基于资源的访问控制以可访问的资源(简单说就是URL)为主体,可扩展性强,权限变动时容易更改。
Java中做角色控制的组件常见的有Apache shiro和Spring Security,它们将软件系统的安全认证相关的功能抽取出来,实现用户身份认证,权限授权、加密、会话管理等功能,组成了一个通用的安全认证框架。
Shiro使用广泛,shiro可以运行在web应用,非web应用,集群分布式应用中越来越多的用户使用shiro。Shiro相对独立,并且使用简单、灵活。
Spring Security(原名Acegi)也是一个开源的权限管理框架,spring security依赖spring运行。除了权限管理,它也有较多其他安全功能,体量较重,使用程度没有Shiro广泛,但它提供的安全防护能力是最全的。
案例项目仅使用interceptor(拦截器)根据URL路径做访问控制的,相对于使用成熟的框架,自写拦截器容易出现更多安全问题。
Interceptor配置在/src/main/java/ltd/newbee/mall/config/NeeBeeMallWebMvcConfigurer.java
中,针对url路径设置了不同的interceptor。
addPathPatterns
表示其中的路径会经过设置的拦截器,excludePathPatterns
则不过该拦截器。其中两个星**
表示匹配任意字符,如果出现一个*
则表示匹配单个路径
看到这里可能有同学会想,此处配置拦截器路径是否会存在绕过的可能性呢?
Spring中拦截器的分配是由DispatcherServlet
来分配的,也就是根据ServletPath
来分配路径,和getRequestURI
无关,也就是不管怎么使用../
来做路径穿越,最终得到的还是ServletPath。如果使用类似%00
空字符进行截断路径,会使得DispatcherServlet
无法将请求分配到正确的Controller,导致请求无效。
管理员访问控制
先来看管理员的权限控制拦截器AdminLoginInterceptor
。
第24行的if判断,若uri开头为/admin
并且当前session中没有loginUser
属性,则跳转到登录页面,注意这里uri是通过getRequestURI()
获取的,关于该方法存在的问题可以查看 https://xz.aliyun.com/t/7544 或 https://joychou.org/web/security-of-getRequestURI.html
/src/main/java/ltd/newbee/mall/interceptor/AdminLoginInterceptor.java:24
在未登录的情况下,访问 http://localhost:8089/index/..;/admin 可直接访问管理后台。此时getRequestURI()
获取的值为/index/..;/admin
,这样就绕过了uri.startsWith("/admin")
的判断,使得if判断为假,不会跳转到登录页面。
我们对if判断进行调试,看到传入的uri为/index/..;/admin/goods/list
,
if判断条件为假,跳转到else语句中
没有cookie的请求中,成功查询到管理员页面中的商品信息
修复方法是将 getRequestURI
改为getServletPath()
再次调试,可以看到通过getServletPath()
获取了正确的请求路径
此时再访问会返回302跳转到登录页面
普通用户访问控制
普通用户的访问控制就简单粗暴了,直接判断当前session中是否存在newBeeMallUser
属性,也就是判断当前用户是否登录。
/src/main/java/ltd/newbee/mall/interceptor/NewBeeMallLoginInterceptor.java:24
这回不管用什么路径都无法绕过登录限制,因为请求对应的session中没有MALL_USER_SESSION_KEY
的值。
我们再回到拦截器的配置中来,看路径配置是否可能存在配置不当的情况,漏掉部分需要登录限制
在Controller中一番搜寻,发现在/src/main/java/ltd/newbee/mall/controller/mall/PersonalController.java:48
中,其中只有/personal
和/personal/updateInfo
设置了规则,而/personal/addresses
未被配置拦截路径。
1 | "/personal/addresses") ( |
访问发现该路径确实没有经过登录拦截器,但是作者没有配置mall/addresses
模版,也就是说这里是个未开发的功能,尚未构成漏洞。但仍存在隐患,若日后开发新功能时没有及时修改拦截器中的配置,就会导致未授权访问。
越权
垂直越权
由于系统没有垂直权限的概念,管理员和普通用户都是独立存在的,管理员进行管理工作,普通用户进行购物、个人信息编辑操作。
这里简单说一下审计思路,审计时弄明白是基于角色还是基于资源做的权限控制还是根本就没做权限控制。再根据其配置内容去找是否存在错误配置的权限,和找权限控制本身是否存在绕过的可能性。
水平越权
在开发时有一个原则是尽量从session中获取用户ID进行查询,这样会避免越权查询漏洞产生。
我们以查看「我的订单」为例,其中user
对象直接从httpSession
中获取,再将user
中的userId
传入后续步骤,最后在数据库中查询。这样一个过程用户侧是没法控制userId
,使得该次数据库查询不可能越权。
/src/main/java/ltd/newbee/mall/controller/mall/OrderController.java:47
我们再来看个人信息修改,此时对数据库进行查询的是mallUser.getUserId()
,而mallUser
是从用户侧获取的,所以该处存在越权修改个人信息漏洞。
/src/main/java/ltd/newbee/mall/controller/mall/PersonalController.java:114
/src/main/java/ltd/newbee/mall/service/impl/NewBeeMallUserServiceImpl.java:73
修改userId
的值即可更改任意用户的个人信息。
修改方案就是将userId
从httpSession
中获取,而不是不通过用户可控的mallUser.getUserId
获取。
以上是查询信息一对一的情况,一对一可以直接通过session获取,而一对多的查询不行,比如查询某订单的情况,一名用户可能存在多个订单,此时就不能仅从session中获取id进行查询,订单号只能通过用户传入。
以订单查询为例,OrderController.java
中第36行,有一行从Session中获取user的语句,第37行,存在user.getUserId()
,是否说明此处会带入session获取到的userId
一同查询呢?
/src/main/java/ltd/newbee/mall/controller/mall/OrderController.java
我们继续跟入newBeeMallOrderService.getOrderDetailByOrderNo()
,发现它直接使用orderNo进行查询,并没有代入从session中获取的userId
,这真是欺骗感情啊!
/src/main/java/ltd/newbee/mall/service/impl/NewBeeMallOrderServiceImpl.java
毫无疑问,此处仅通过用户传入的orderNo
获取订单信息一定存在越权查询。
我们新建一个test1用户,查看「我的订单」,空空如也
通过test1访问他人的订单号,发现成功访问,可以看到订单细节
修复方案
表结构在设计之初应考虑订单和用户对应的关系,案例项目数据库订单表结构部分信息如下:
order_id | order_no | user_id | total_price | pay_status | pay_type | extra_info |
---|---|---|---|---|---|---|
1 | 15688187285093508 | 1 | 2492 | 1 | 2 | |
2 | 15885800076852966 | 10 | 2245 | 1 | 1 |
可以看到存在其中order_no
和user_id
存在对应关系,所以将orderNo
和从session中获取的userId
同时查询可解决越权问题。
1 | select * forom tb_newbee_mall_order where order_no = #{orderNo,jdbcType=VARCHAR} and user_id = #{userId,jdbcType=LONG} and is_deleted=0 limit 1 |