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注入

1
2
3
4
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
filters: wall

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

方案二的实现代码

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@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类读取配置文件中需要跳过检查的接口数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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数组即可

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