背景
测试反馈反馈接口出现了SQL注入问题,请求url为
/searchDictionary?page=1&size=20&searchWord=%7bbase%7d%22%20or%20version()%20like%20%22%&classificationId=&sortColumn=&sortDirection=desc
测试人员反馈,通过该参数调用接口,返回了全部数据
问题排查
首先查看相关的SQL执行代码
Specification specification = new Specification<DataDictionary>() {
@Override
public Predicate toPredicate(Root<DataDictionary> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
List<Predicate> predicates_and = new ArrayList<>();
predicates_and.add(cb.equal(root.get("deleted"), false));
if (StringUtils.isNotBlank(classId)) {
predicates_and.add(cb.equal(root.get("classificationId"), classId));
}
List<Predicate> predicates_or = new ArrayList<>();
if (StringUtils.isNotBlank(searchWord)) {
predicates_or.add(cb.like(root.get("name"), "%" + searchWord + "%"));
predicates_or.add(cb.like(root.get("code"), "%" + searchWord + "%"));
}
if (CollectionUtils.isNotEmpty(predicates_or)) {
predicates_and.add(cb.or(predicates_or.toArray(new Predicate[0])));
}
return cb.and(predicates_and.toArray(new Predicate[0]));
}
};
return dataDictionaryRepository.findAll(specification, pageable);
代码采用了JPA的Predicate进行数据拼接,并执行查询,因此在此处不会出现SQL注入的问题。
接着查看日志,发现在Controller层,接受到的参数中,searchWord的值为null,怀疑是Spring在处理过程中,SpringSecurity做过什么处理,于是决定查看Servlet中接受到参数是什么样的。
将断点设置在DispatcherServlet的doGet方法中,查看request.getParameterMap()的结果,发现此时Map的长度为5,并且Map中也没有searchWork字段,此时怀疑是数据问题导致servlet无法对参数进行映射。
在线将参数进行UrlDecode操作,得到结果为{base}" or version() like "%
,发现此参数并无问题,再采用Java的UrlDecoder进行解码,程序报错
java.lang.IllegalArgumentException: URLDecoder: Incomplete trailing escape (%) pattern
at java.net.URLDecoder.decode(URLDecoder.java:187)
由报错信息可见,有一个%有问题,重新审视地址上的参数,发现最后一个%并未被encode。
由此猜测问题大致形成的原因为,参数在进行UrlDecode时出现了异常,但是异常并未进行抛出,Servlet在进行转换时,将报错的参数进行了忽略,Spring在进行转换映射时无法获取到该参数,从而导致searchWord参数为空,在执行SQL拼接时,就没有了对应的查询条件,因此查询出了所有的数据。
因此对这种类型的参数要进行控制,request.getQueryString()可以正常获取到请求的数据字符串,因此考虑通过QueryString中Key的长度和ParameterMap的Size进行比较,判断参数是否合规。
考虑到DispatcherServlet中doGet,doPost等方法都加上了final关键字,不好重写,因此采用Filter进行处理。
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
String queryString = request.getQueryString();
if (StringUtils.isNotBlank(queryString)){
Set<String> params = new HashSet<>();
String[] split = StringUtils.split(queryString, "&");
for (String s : split) {
if (StringUtils.isNotBlank(s)){
params.add(s.split("=")[0]);
}
}
if (request.getParameterMap().size()!=params.size()){
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setStatus(403);
response.setHeader("Content-Type","application/json;charset=UTF-8");
response.getWriter().write("{\n" +
" \"errcode\": 403,\n" +
" \"errmsg\": \"非法参数\",\n" +
"}");
return;
}
}
filterChain.doFilter(servletRequest, servletResponse);
}