记一次URL参数导致的bug

young 37 2025-04-17

背景

测试反馈反馈接口出现了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);
 }