项目上线前做了安全扫描,安全部门扫描出一个关于越权的问题。这个问题是在刚开始开发接口的时候没有考虑到的一个事情。(此项目是有关于用户所拥有的项目和活动权限的问题。)
问题描述
首先一个用户进到列表页面,点击编辑一个项目,然后通过抓包,得到调取后台接口的参数列表信息,然后知道参数列表里有id信息,这样用户如果知道其他项目id,或者这有查看权限,没有编辑项目的项目id,通过修改id,就可以越权,对没有权限的项目进行修改。
实现思路
- 首先用户登录时,通过Shiro框架,将用户信息放入Subject中,并将该用户对用的项目id和权限id放入redis中。
- 再分析并整理对应相关越权的接口,将这些接口整理,并且添加拦截器,将这些接口进行拦截。
- 拦截之后,从Shiro框架的Subject获取用户信息,根据用户信息查询该用户对应的项目id或活动id。
- 因为有些接口对项目操作,有的接口只对活动操作,有的接口对这两个都操作,所以我选择先判读项目id是否为空,然后再判读项目id存不存在在redis中,不存在直接返回false,若存在,再判断活动id的情况。
具体代码
1.拦截器配置类 CrossPowerConfig.java
import com.exp.core.interceptor.CrossPowerInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.validation.MessageCodesResolver;
import org.springframework.validation.Validator;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.config.annotation.*;
import java.util.List;
@Configuration
public class CrossPowerConfig implements WebMvcConfigurer {
@Autowired
private CrossPowerInterceptor crossPowerInterceptor;
@Override
public void configurePathMatch(PathMatchConfigurer pathMatchConfigurer) {
}
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer contentNegotiationConfigurer) {
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer asyncSupportConfigurer) {
}
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer defaultServletHandlerConfigurer) {
}
@Override
public void addFormatters(FormatterRegistry formatterRegistry) {
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
// addPathPatterns("/**") 表示拦截所有的请求,
registry.addInterceptor(crossPowerInterceptor).addPathPatterns(
//要拦截的接口地址
"/aaa/bbb",
"/aaa/ccc"
...
);
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry resourceHandlerRegistry) {
}
@Override
public void addCorsMappings(CorsRegistry corsRegistry) {
}
@Override
public void addViewControllers(ViewControllerRegistry viewControllerRegistry) {
}
@Override
public void configureViewResolvers(ViewResolverRegistry viewResolverRegistry) {
}
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> list) {
}
@Override
public void addReturnValueHandlers(List<HandlerMethodReturnValueHandler> list) {
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> list) {
}
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> list) {
}
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> list) {
}
@Override
public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> list) {
}
@Override
public Validator getValidator() {
return null;
}
@Override
public MessageCodesResolver getMessageCodesResolver() {
return null;
}
}
2.拦截器执行操作类 CrossPowerInterceptor.java
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class CrossPowerInterceptor implements HandlerInterceptor {
private static Logger log = LoggerFactory.getLogger(CrossPowerInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {
log.info("--------------------->进入越权相关拦截器---------------------");
dosomething...
//判断符合条件
boolean flag=true;
if(flag){
return true;
}else {
return false;
}
}
@Override
public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
}
}
当到这以为大功告成的时候,发现有一个接口,传数据的形式是JSON,当在拦截器里request.getinputstream()的时候,通过了拦截器,返回了true,进入接口的时候request.getinputstream()就获取为空,然后想到request.getinputstream()只能获取一次的问题,所以又考虑添加过滤器,在过滤器里将获取请求中的流,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。
这时候整体的流程就变成了
添加的具体代码如下:
1.HttpServletRequestReplacedFilter.java
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
public class HttpServletRequestReplacedFilter implements Filter {
@Override
public void destroy() {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
if(request instanceof HttpServletRequest) {
requestWrapper = new RequestReaderHttpServletRequestWrapper((HttpServletRequest) request);
}
//获取请求中的流如何,将取出来的字符串,再次转换成流,然后把它放入到新request对象中。
// 在chain.doFiler方法中传递新的request对象
if(requestWrapper == null) {
chain.doFilter(request, response);
} else {
chain.doFilter(requestWrapper, response);
}
}
@Override
public void init(FilterConfig arg0) throws ServletException {
}
}
2.RequestReaderHttpServletRequestWrapper.java
import com.exp.util.HttpHelper;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
public class RequestReaderHttpServletRequestWrapper extends HttpServletRequestWrapper{
private final byte[] body;
public RequestReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return bais.read();
}
@Override
public boolean isFinished() {
return false;
}
@Override
public boolean isReady() {
return false;
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
}
}
3.HttpHelper.java
import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
public class HttpHelper {
public static String getBodyString(HttpServletRequest request) throws IOException {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}
4.启动类 App.java
import com.exp.core.filter.HttpServletRequestReplacedFilter;
import com.exp.core.listener.MyApplicationContextListener;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* 程序启动入口
*/
@SpringBootApplication
@EnableTransactionManagement
public class App {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(App.class);
//注册监听器
app.addListeners(new MyApplicationContextListener());
app.run(args);
}
/**
* 功能描述:
* 设置过滤,getinputstream取request数据时,再放回request中。
* @Param: []
* @Return: org.springframework.boot.web.servlet.FilterRegistrationBean
* @Author:
* @Date: 2020/9/14 14:58
*/
@Bean
public FilterRegistrationBean httpServletRequestReplacedRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new HttpServletRequestReplacedFilter());
registration.addUrlPatterns(
//要拦截的接口地址
"/aaa/bbb",
"/aaa/ccc"
...
);
registration.addInitParameter("paramName", "paramValue");
registration.setName("httpServletRequestReplacedFilter");
registration.setOrder(1);
return registration;
}
}