越权漏洞分类

  1. 未授权访问:本来没有账号(即没有某个功能权限),但是通过越权操作,获取了某个功能权限
  2. 水平越权(横向越权):通过越权操作,能操作其他同等权限账号的数据
  3. 垂直越权(纵向越权):低权限用户通过越权操作获取了高权限

测试方法

未授权访问

最简单的测试方法就是不登录用户,直接访问要测试的需要登录的功能模块,如果可以访问成功,则说明存在漏洞。

springboot apo 判断水平越权 springboot越权控制_越权漏洞


我们期望的应该是:

springboot apo 判断水平越权 springboot越权控制_数据_02

有些系统是在cookie中添加一个类似userId的字段来标识是否登录成功,这样的系统可以通过伪造cookie信息来进行测试。

springboot apo 判断水平越权 springboot越权控制_java_03


我们期望的应该是:

springboot apo 判断水平越权 springboot越权控制_数据_04

水平越权

水平越权常见于业务系统中,例如对用户信息或者订单信息进行增删改查操作时,由于用户编号或者订单编号有规律可循(有序递增,订单编号常发现以日期开头后面再接几位有序增长的数字,类似20200520xxxx1,20200520xxxx2),测试人员通过burpsuite的 intruder对目标参数进行遍历测试即可,如发现用户A能操作用户B的用户信息或者订单信息,则说明存在漏洞,其中用户A和用户B是具有同等权限的用户。

springboot apo 判断水平越权 springboot越权控制_拦截器_05


我们期望的应该是:

springboot apo 判断水平越权 springboot越权控制_spring_06

垂直越权

准备两个用户,用户A是一个低权限角色的用户,用户B是管理员角色的用户,对某些应该只有管理员角色才有访问权限的功能模块,使用用户A登录进行访问,如果能正常访问,则说明漏洞存在。这里有些系统是前后端分离,可能直接访问前端用户A是看不到用户B的某些菜单的,可以先使用用户B登录系统,将相关链接或接口记录下来,之后再用用户A登录,来直接访问记录下来的链接或相关接口。

springboot apo 判断水平越权 springboot越权控制_拦截器_07


我们期望的应该是:

springboot apo 判断水平越权 springboot越权控制_拦截器_08

解决方案

对于越权问题的解决思路是对用户访问的资源进行拦截,判断当前用户是否有该资源的权限,如果有则放行,请求用户要访问的资源,如果没有则直接返回相应的提示,不再请求用户要访问的资源。
在SpringWeb项目中要解决越权问题,大体上可以有2种解决方案。

  1. 使用Servlet的过滤器,对请求资源(可以是所有的资源,包括一些静态的资源)进行过滤拦截,对当前用户和当前请求做权限判断(静态资源也需要拦截的使用这种方案)
  2. 使用SpringMVC的拦截器,对请求资源(只能拦截Controller接口资源)进行过滤拦截,对当前用户和当前请求做权限判断(Spring项目推荐使用这种方案)
    这2种方案主要是来解决未授权访问和垂直越权的问题,对于水平越权问题本质上还是要对访问数据做数据权限的控制或者对访问资源做用户隔离(操作数据时带上userId进行操作)这样就可以保证操作不到其他人的数据了。当然如果专门有数据权限的控制,则和垂直越权问题一样来解决。

以下给出两种解决方案的可参考的代码实现(实际使用过程中,根据自身项目情况进行改造)
项目环境:
JDK 1.8
Maven 3.6.3
SpringBoot项目,版本如下

<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.5</version>
        <relativePath/>
  </parent>
  <dependencies>
	<!--springboot web 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.6.5</version>
        </dependency>
        <!-- json解析库 -->
         <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.78</version>
        </dependency>
  <dependencies>

Servlet过滤器代码实现

这里提供的是基于注解方式实现的。
1 AuthorizeFilter过滤器

/**
 * filterName:指定过滤器的名字
 * urlPatterns: 指定需要过滤的地址
 * @author luojj
 * @since 2022-04-11 18:27
 */
@WebFilter(filterName = "authorizeFilter",urlPatterns = {"/user/*"})
public class AuthorizeFilter implements Filter {

    private static final String USER_KEY = "APP_USER_ID";

    @Autowired
    UserService userService;

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        final String userId = request.getHeader(USER_KEY);
        if(canAccess(userId,request.getRequestURI())){
            filterChain.doFilter(servletRequest,servletResponse);
            return;
        }
        //处理鉴权失败
        //final Rsp<?> rsp = Rsp.fail(403, "无权访问");
        //这里实际项目上可以使用项目定义的Rsp对象
        Map<String,Object> rsp = new HashMap<>(2);
        rsp.put("code",403);
        rsp.put("msg","无权访问");
        servletResponse.setCharacterEncoding("UTF-8");
        servletResponse.setContentType("application/json; charset=utf-8");
        final PrintWriter writer = servletResponse.getWriter();
        writer.write(JSON.toJSONString(rsp));
    }

    private boolean canAccess(String userId,String path) {
        if(userId == null){
            return false;
        }
        //此处是具体查询用户具有哪些权限的,具体使用自己的项目上的Service方法
        final Set<String> userAccessUrls = userService.getUserAccessUrls(userId);
        return userAccessUrls.contains(path);
    }
}

2 需要在springboot启动类上添加以下注解,保证spring可以扫描到我们自己实现的过滤器
@ServletComponentScan(basePackageClasses = {AuthorizeFilter.class})

对于方案1至此就实现完成了。

SpringMVC拦截器代码实现

1 AuthorizeInterceptor 拦截器

@Component
public class AuthorizeInterceptor implements HandlerInterceptor {

    private static final String USER_KEY = "APP_USER_ID";

    @Autowired
    UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(!(handler instanceof HandlerMethod)){
            return true;
        }
        final String userId = request.getHeader(USER_KEY);
        if(canAccess(userId,request.getRequestURI())){
            return true;
        }
        //处理鉴权失败
//        final Rsp rsp = Rsp.fail(403, "无权访问");
        Map<String,Object> rsp = new HashMap<>(2);
        rsp.put("code",403);
        rsp.put("msg","无权访问");
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        final PrintWriter writer = response.getWriter();
        writer.write(JSON.toJSONString(rsp));
        return false;
    }

    private boolean canAccess(String userId,String path) {
        if(userId == null){
            return false;
        }
        final Set<String> userAccessUrls = userService.getUserAccessUrls(userId);
        return userAccessUrls.contains(path);
    }

}

2 鉴权拦截器相关MVC配置

@Configuration
@AllArgsConstructor
public class AuthorizeWebMvcConfig implements WebMvcConfigurer {

    AuthorizeInterceptor authorizeInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(authorizeInterceptor)
                .addPathPatterns("/**")
                //这是指定排除哪些访问地址不拦截
                .excludePathPatterns("/swagger-resources","/v2/api-docs");

    }
}

对于第2种方案至此就实现完成了。