mybatis层面限制SQL注入

最近根据公司的要求需要限制一波SQL注入的问题,因为公司有自己的数据库访问层组件,使用的数据库连接池为druid。
其实在druid的com.alibaba.druid.wall.WallFilter中提供了对sql依赖注入的检查,但是最终有下面几个原因我们没有
在druid层面来解决这个问题:

  • 我们依赖的druid版本1.0.8太低了,查看了一下druid后面不少版本的改进,对sql解析这块有大量的优化,因此贸然升级一个版本风险有点高
  • 我们想修改一下WallConfig的配置,但是在1.0.8版本的druid中不太好获取WallConfig这个类的实例,高版本的druid优化了这个问题。而且在druid中MySqlWallProvider类是在com.alibaba.druid.wall.spipackage下面定义的,但是在WallFilter#init()方法中缺并没有使用spi等方式来拿这个类。
  • 我们想最小化改动,尽量减少测试成本

在上面3个原因的考虑下,我们放弃了这种方案,转而将目光放在了公司统一使用的Mybatis上。

在Mybatis的org.apache.ibatis.scripting.xmltags.TextSqlNode中有一个字段为injectionFilter

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
public class TextSqlNode implements SqlNode {
private String text;
private Pattern injectionFilter;

//...其他代码省略
private static class BindingTokenParser implements TokenHandler {

private DynamicContext context;
private Pattern injectionFilter;

public BindingTokenParser(DynamicContext context, Pattern injectionFilter) {
this.context = context;
this.injectionFilter = injectionFilter;
}

@Override
public String handleToken(String content) {
Object parameter = context.getBindings().get("_parameter");
if (parameter == null) {
context.getBindings().put("value", null);
} else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
context.getBindings().put("value", parameter);
}
Object value = OgnlCache.getValue(content, context.getBindings());
String srtValue = (value == null ? "" : String.valueOf(value)); // issue #274 return "" instead of "null"
checkInjection(srtValue);
return srtValue;
}

private void checkInjection(String value) {
if (injectionFilter != null && !injectionFilter.matcher(value).matches()) {
throw new ScriptingException("Invalid input. Please conform to regex" + injectionFilter.pattern());
}
}
}
}

可以看到在handleToken方法中处理完$标签以后,会调用checkInjection来检查替换的内容,在这块只要我们正确的设置injectionFilter属性那么也可以达到限制
$可以替换的内容的目的。默认情况下injectionFilter的属性一直是null。

目前mybatis其实并没有地方可以直接设置这个属性,为了设置这个属性,其实我们也额外做了不少的事情:

  • 我们自己写了一个XxxXMLLanguageDriver,这个类继承了org.apache.ibatis.scripting.xmltags.XMLLanguageDriver, 然后覆盖了其中的createSqlSourcecreateSqlSource
    方法。
  • 我们重写了一个XxxXMLScriptBuilder, 用来替换org.apache.ibatis.scripting.xmltags.XMLScriptBuilder
  • 通过Spring的BeanPostProcessor,然后在postProcessAfterInitialization回调中,针对所有的SqlSessionFactory实例,拿到它的Configuration属性,并调用configuration.setDefaultScriptingLanguage(XxxXMLLanguageDriver.class),使得我们自己的XxxXMLLanguageDriver生效。

贴一下XxxXMLLanguageDriver核心代码吧:

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
public class XxxXMLLanguageDriver extends XMLLanguageDriver {

static volatile Pattern injectionFilter;

private static final Pattern DEFAULT_INJECTION_FILTER = Pattern.compile(DEFAULT_SQL_INJECTIONFILTER_REGEX);

private static final Logger logger = LoggerFactory.getLogger(XxxXMLLanguageDriver.class);

static {
//...此处代码省略,此处做的事情就是通过公司的配置中心动态的设置injectionFilter
}

/**
* 此处相比于{@link XMLLanguageDriver#createSqlSource(Configuration, XNode, Class)}仅仅修改了使用的
* {@link XMLScriptBuilder}, 改为使用自己的{@link XxxXMLScriptBuilder}
*/
@Override
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XxxXMLScriptBuilder builder = new XxxXMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}

/**
* 此处相比于{@link XMLLanguageDriver#createSqlSource(Configuration, String, Class)},
* 仅仅修改了{@link TextSqlNode}的创建
*/
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
// issue #3
if (script.startsWith("<script>")) {
XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
}
else {
// issue #127
script = PropertyParser.parse(script, configuration.getVariables());
TextSqlNode textSqlNode = new TextSqlNode(script, injectionFilter);
if (textSqlNode.isDynamic()) {
return new DynamicSqlSource(configuration, textSqlNode);
}
else {
return new RawSqlSource(configuration, script, parameterType);
}
}
}

private static Pattern compile(String regex) {
try {
return Pattern.compile(regex);
}
catch (PatternSyntaxException e) {
logger.error("XxxXMLLanguageDriver compile injection filter regex [{}] error", regex, e);
return DEFAULT_INJECTION_FILTER;
}
}
}

写在后面,当初在弄这个事情的时候,我也在好奇为啥mybatis没有将这个属性暴露出来,然后翻看了一下mybatis的更新记录,果然又发现:

在issueshttps://github.com/mybatis/mybatis-3/issues/117中看到了关于这个属性的讨论,而且有几次commit记录:

可以看到作者也在纠结过,不过最终结论如同上面issues最终定论的:

The rule of thumb is: mark the data as data.

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×