MyBatis源码分析之防SQL注入
Posted 叶长风
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MyBatis源码分析之防SQL注入相关的知识,希望对你有一定的参考价值。
MyBatis源码分析之防SQL注入
这一节来讲下MyBatis的防SQL注入,SQL注入大多数也会比较清楚,就是SQL参数对应的字段值时插入混合SQL,如 ** username = or 1= 1** 这种,如果有更恶劣的,带上drop database 这种都是有可能的,所以一般SQL都会进行一定防注入处理,MyBatis其实用法大都清楚,就是**#paras和$paras**两种用法,以前我就是会用,但是具体原理咱也没看过,在一次面试中,被别人问到过具体用法,注入原理,处理原理等等,当时就是根据自己的印象去回答了,但是对于MyBatis与数据库具体处理SQL注入却不是很熟悉的,这一节就来详细讲解下,记录一下。
#1. #paras与$paras用法
对于**#paras和KaTeX parse error: Expected 'EOF', got '#' at position 23: …**写两个用法吧,一个根据**#̲paras**来查询,一种…paras**来查询,如下:
@ResultMap("BaseResultMap")
@Select("select * from user where username = #username")
User getUserByParas1(@Param("username") String username);
@ResultMap("BaseResultMap")
@Select("select * from user where username = $username")
List<User> getUserByParas2(@Param("username") String username);
调用这两个方法的程序为:
User user = userMapper.getUserByParas1("xiaxuan");
List<User> users = userMapper.getUserByParas2(" 1 or 1 = 1");
System.out.println(user);
System.out.println(users);
运行结果为:
运行结果和想象中差不多,在conf.xml中添加打印sql的配置.
<settings>
<!-- 打印查询语句 -->
<setting name="logImpl" value="STDOUT_LOGGING" />
</settings>
再运行一次看看,如下图:
第一个sql是进行了占位符处理,第二个是直接拼出了sql语句。
上述就是演示#paras调用和和sql注入的过程,下面开始讲两者在实现过程中的原理。
2. 初始化源码分析
首先看看这两个方法是怎么加载的,这还是要回到注解方法的解析中,在前文中我们知在解析注解时,会将当前sql包装成SqlSource对象,然后在查询时使用,代码如下:
void parseStatement(Method method)
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
.....
我们进getSqlSourceFromAnnotations方法中看看。
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver)
try
Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
if (sqlAnnotationType != null)
if (sqlProviderAnnotationType != null)
throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
else if (sqlProviderAnnotationType != null)
Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
return null;
catch (Exception e)
throw new BuilderException("Could not find value method on SQL annotation. Cause: " + e, e);
此处我们只分析带有@Select注解的方法,所有就是进入if条件中进行处理,这里再进入buildSqlSourceFromStrings方法。
private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver)
final StringBuilder sql = new StringBuilder();
for (String fragment : strings)
sql.append(fragment);
sql.append(" ");
return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
最后通过languageDriver返回SqlSource,进入createSqlSource方法中。
@Override
public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType)
SqlSource source = super.createSqlSource(configuration, script, parameterType);
checkIsNotDynamic(source);
return source;
@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);
if (textSqlNode.isDynamic())
return new DynamicSqlSource(configuration, textSqlNode);
else
return new RawSqlSource(configuration, script, parameterType);
在两个方法中做了最后的sql包装,在createSqlSource方法中,第一种是处理带有**
public boolean isDynamic()
DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
GenericTokenParser parser = createParser(checker);
parser.parse(text);
return checker.isDynamic();
这里实例化了一个DynamicCheckerTokenParser对象,但是进这个实例化方法中可以看到并没有做什么事情,主要是createParser中,转到createParser方法。
private GenericTokenParser createParser(TokenHandler handler)
return new GenericTokenParser("$", "", handler);
然后下一步对是否有"$", ""进行判断,parser.parse(text);,最后返回是否返回是否是动态sql,在这毫无疑问的是我们前面写的方法中,第一个是返回的RawSqlSource对象,第二个返回的是DynamicSqlSource对象,这两个对象这里不做分析,等会再执行时再来看是如何对sql进行处理的。
3. mapper执行源码分析
上面分析完了在配置时的sql过程,现在分析在select语句执行时对SqlSource的解析,在前面文章分析中,我们知所有的查询基本是调用的SelectList方法,然后在最后调用doQuery方法之前会获得BoundSql对象,我们的源码分析就是从这开始。
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException
BoundSql boundSql = ms.getBoundSql(parameter);
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
这里可以对上面两个代码调试一下代码,BoundSql对象如下图:
方法1:
这里#paras已经替换成了占位符,然后对应参数映射在parameterObject对象中,这里就先不展示第二个方法执行到这的BoundSql对象了,先把这个分析清楚吧,从上面图与源码中我们知最终的处理方法在,ms.getBoundSql中,进入到getBoundSql方法。
public BoundSql getBoundSql(Object parameterObject)
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
if (parameterMappings == null || parameterMappings.isEmpty())
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
// check for nested result maps in parameter mappings (issue #30)
for (ParameterMapping pm : boundSql.getParameterMappings())
String rmId = pm.getResultMapId();
if (rmId != null)
ResultMap rm = configuration.getResultMap(rmId);
if (rm != null)
hasNestedResultMaps |= rm.hasNestedResultMaps();
return boundSql;
进入到sqlSource.getBoundSql(parameterObject);方法中,在前文中我们只分析了DynamicSqlSource,但是并没有对RawSqlSource进行分析,在这顺便说下RawSqlSource,直接看RawSqlSource的构造方法。
public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType)
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> clazz = parameterType == null ? Object.class : parameterType;
sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
对sql进行了进一步的解析,进入到parse方法中。
public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters)
ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
GenericTokenParser parser = new GenericTokenParser("#", "", handler);
String sql = parser.parse(originalSql);
return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
此处判断sql中是否含有"#""",parser.parse(originalSql)中解析源码的过程比较复杂,没有太多可以说的,但是得说下占位符的替换,在替换参数是最终会调用到SqlSource中的handlerToken方法。
@Override
public String handleToken(String content)
parameterMappings.add(buildParameterMapping(content));
return "?";
此处进行了参数的替换,以及对字段的映射。
然后最终返回StaticSqlSource对象,所以此处RawSqlSource最后还是返回StaticSqlSource对象,然后在StaticSqlSource对象中执行getBoundSql方法,可以进StaticSqlSource的getBoundSql中看看。
@Override
public BoundSql getBoundSql(Object parameterObject)
return new BoundSql(configuration, sql, parameterMappings, parameterObject);
最终对替换参数后的sql、参数、参数映射以及对应值,构造BoundSql对象返回。
此处几乎分析的还是加载配置时的代码,还是回到query执行处,在获取到boundSql对象后,继续执行query方法,最终调用PreparedStatement对象执行execute方法,这里传入的sql是需要进行预编译的sql,同时传入参数,因此此处参数不会和sql一起执行,纯粹是sql对应查询字段值。
再来分析动态sql,再次回到query方法,如下图:
此处已经是拼好的sql,但是我们看ms中的SqlSource对象。
此时还没有对sql进行拼接,所以在获取BoundSql时,对参数就进行了替换,在上文中我们知此时的SqlSource对象为DynamicSqlSource对象,直接进入getBoundSql方法中。
public BoundSql getBoundSql(Object parameterObject)
DynamicContext context = new DynamicContext(configuration, parameterObject);
rootSqlNode.apply(context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
for (Map.Entry<String, Object> entry : context.getBindings().entrySet())
boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
return boundSql;
此处构建DynamicContext上下文,最终处理还是在apply方法中。
@Override
public boolean apply(DynamicContext context)
GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
context.appendSql(parser.parse(text));
return true;
在apply方法中,对text进行处理,核心代码在parser.parse(text)中,这里处理的逻辑就不讲了,进去后可以看到对参数的替换最终组装成当前的sql,从而实现了sql 的注入。
在后面的query方法就不再进行分析了,基本在组装出sql后一切都明了了。
以上是关于MyBatis源码分析之防SQL注入的主要内容,如果未能解决你的问题,请参考以下文章