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用法


对于**#parasKaTeX 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注入的主要内容,如果未能解决你的问题,请参考以下文章

Mybatis源码分析(自己动手造轮子)

Mybatis-Plus的应用场景及注入SQL原理分析

「PHP开发APP接口实战009」日常安全防范之防SQL入和XSS攻击

Mybatis-Plus的应用场景及注入SQL原理分析

MyBatis 源码解析:SQL 语句的执行机制

MyBatis如何防止SQL注入