Java防止SQL注入

架构和环境

  • spring-cloud版本:Dalston.SR1
  • spring-boot版本:1.5.3.RELEASE;
  • ORM:spring-data-jpa
  • 数据库连接池:Druid

问题描述

系统被检测出多处大量接口有SQL注入风险,并且指明了存在多种注入的方式

原因

框架架构初期约定的JPA与数据库交互没有被开发人员恪守,存在大量直接拼接的SQL执行语句,既没有预编译,也没有做危险SQL关键词的统一过滤

解决方案

  • 方案一:采用预编译方式运行SQL,如今基本所有数据库都支持预编译,所以直接使用数据库连接包中的预编译类即可实现,不过所有拼接SQL的代码都需要更改,工作量非常巨大,此篇不再赘述,百度java SQL预编译即可
  • 方案二:在网关拦截处做处理,拦截获取的请求的所有参数,判断参数是否有SQL注入风险的关键词,有危险关键词就一律报错不予执行,这样侵入性太强,大量参数传入可能会给系统造成一定负担,而且有些关键词存在SQL注入风险但是还是需要使用的
  • 方案三:这个方案是后来才发现的,因为系统使用的数据库连接池是Druid,Druid自带防止SQL注入的配置,在第一个方案被否决,第二个方案发布测试后很多接口被测试和开发怼,不得已的情况下发现了这个最佳的解决方案

实现代码

开门见山,最佳方案三的配置

添加配置文件中Druid配置filters= wall表示防止SQL注入

spring:
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    filters: wall

当然有的项目不是使用Druid做数据库连接池,其他的连接池有些同样自带防SQL注入的配置,可以检索一下。

后期测试过程中发现MySQL有个神奇的地方,如下的SQL查询了全表而且跳过了Druid:

select * from user where login_name = '张三'or'1155';

这样的SQL在MySQL不仅没有报错居然还是全表查询所有数据的,or后面接任意的数字字符串,甚至or两边都不需要空格,SQLserver找人测试不存在的这种情况,oracle不清楚,可以自行测试,所以MySQL中没法直接用这个配置,后来还是添加一些关键词过滤

方案二的实现代码

SqlInjectionFilter类继承ZuulFilter重写run方法(此类还带有预防XSS攻击的代码,如果配置了Druid配置,可以删除此处预防sql注入相关代码,改为预防XSS攻击的类)

@Component
@Slf4j
public class SqlInjectionFilter extends ZuulFilter {

    @Value("${custom.sql-injection-filter.enabled}")
    private boolean enabled;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    // 自定义过滤器执行的顺序,数值越大越靠后执行,越小就越先执行
    @Override
    public int filterOrder() {
        return FilterConstants.PRE_DECORATION_FILTER_ORDER - 2;
    }

    @Override
    public boolean shouldFilter() {
        return enabled;
    }

    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        try {
            //判断需要跳过过滤的url
            for (String skipUrl : SqlInjectionConfig.getSkipUrls()) {
                if(-1 != request.getRequestURI().indexOf(skipUrl)){
                    return null;
                }
            }
            // 执行过滤逻辑
            InputStream in = ctx.getRequest().getInputStream();
            String body = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
            if (StringUtils.isBlank(body)) {
                body = JSONObject.fromObject(request.getParameterMap()).toString();
                if (StringUtils.isBlank(body)) {
                    return null;
                }
            }
            Map<String, Object> stringObjectMap = cleanXSS(body);
            JSONObject json = JSONObject.fromObject(stringObjectMap);
            String newBody = json.toString();
            //  如果存在sql注入,直接拦截请求
            if (newBody.contains("forbid")) {
                setUnauthorizedResponse(ctx);
            }
            final byte[] reqBodyBytes = newBody.getBytes();
            ctx.setRequest(new HttpServletRequestWrapper(request) {

                @Override
                public ServletInputStream getInputStream() throws IOException {
                    return new ServletInputStreamWrapper(reqBodyBytes);
                }

                @Override
                public int getContentLength() {
                    return reqBodyBytes.length;
                }

                @Override
                public long getContentLengthLong() {
                    return reqBodyBytes.length;
                }
            });
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private Map<String, Object> cleanXSS(String value) {
        value = value.replaceAll("<", "& lt;").replaceAll(">", "& gt;");
        value = value.replaceAll("\\(", "& #40;").replaceAll("\\)", "& #41;");
        value = value.replaceAll("'", "& #39;");
        value = value.replaceAll("eval\\((.*)\\)", "");
        value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\"");
        value = value.replaceAll("script", "");
        value = value.replaceAll("[*]", "[" + "*]");
        value = value.replaceAll("[+]", "[" + "+]");
        value = value.replaceAll("[?]", "[" + "?]");

        String badStr = "'|and|exec|execute|insert|select|delete|update|count|drop|chr|mid|master|truncate|" +
                "char|declare|sitename|net user|xp_cmdshell|;|or|+|create|table|from|grant|group_concat|" +
                "column_name|information_schema.columns|table_schema|union|where|--|,|like|//|/|%|#";

        JSONObject json = JSONObject.fromObject(value);
        String[] badStrs = badStr.split("\\|");
        Map<String, Object> map = json;
        Map<String, Object> mapjson = new HashMap<>();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            String value1 = entry.getValue().toString().toLowerCase();
            for (String bad : badStrs) {
                if (-1 != value1.indexOf(bad)) {
                    log.info("拦截的参数#####################"+value1);
                    log.info("拦截的关键字#####################"+bad);
                    value1 = "forbid";
                    mapjson.put(entry.getKey(), value1);
                    break;
                } else {
                    mapjson.put(entry.getKey(), entry.getValue());
                }
            }
        }
        return mapjson;
    }

    private void setUnauthorizedResponse(RequestContext requestContext) {
        Gson gson = new Gson();
        BaseResponse result = new BaseResponse();
        result.setCode(ErrorCode.ERR_GLOBAL_PARA_CHECK);
        result.setMsg("SQL Injection Risk");
        requestContext.setResponseBody(gson.toJson(result));
    }

}

SqlInjectionConfig类读取配置文件中需要跳过检查的接口数组

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@ConfigurationProperties(prefix = "custom.sql-injection-filter")
@Component
public class SqlInjectionConfig {


    public static String[] skipUrls;

    public static String[] getSkipUrls() {
        return skipUrls;
    }

    public void setSkipUrls(String[] skipUrls) {
        SqlInjectionConfig.skipUrls = skipUrls;
    }

}

yml配置文件,如果想要关闭SQL注入检测将enable改为false即可,如果想要某个接口跳过检测,添加到skipUrls数组即可

custom:
  sql-injection-filter:
    enabled: true
    skipUrls: /api-file,/api-source,/api-user/user/info.do

发表评论

邮箱地址不会被公开。 必填项已用*标注