架构和环境
- 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