深入源码,探究#、$号替换符的区别

在Mybatis的日常使用过程中以及在一些技术论坛上我们都能常常听到,不要使用$符号来进行SQL的编写,要使用#符号,否则会有SQL注入的风险。那么,为什么在使用$符号时会有注入的风险呢,以及#号为什么不会有风险呢?这一期我们来从源码分析一下。

$号占位符

在Mybatis替换SQL占位符时,会针对$#号进行解析替换操作。然而对于$号来说,仅仅只会将该参数对应的值拼接在SQL中而已。

前置知识

在Mybatis中,SQL会被解析成一个个的SqlNode,对于不同的SqlNodeMybatis的解析处理都是不一样的。
一般情况来说,SQL中存在$号的话,都会被解析成TextSqlNode

解析并替换

Mybatis中,解析TextSqlNode的占位符主要使用到两个类

  1. GenericTokenParser:用于查找SQL中具体的占位符以及占位符代表的属性名
  2. TokenHandler:根据占位符的属性名获取对应的值
public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    // 找到$号所在的位置
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    // 占位符中的变量名
    StringBuilder expression = null;
    do {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if ((end <= offset) || (src[end - 1] != '\\')) {
            expression.append(src, offset, end - offset);
            break;
          }
          // this close token is escaped. remove the backslash and continue.
          expression.append(src, offset, end - offset - 1).append(closeToken);
          offset = end + closeToken.length();
          end = text.indexOf(closeToken, offset);
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          // 从TokenHandler中解析出变量名对应的参数值
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    } while (start > -1);
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

在这个parse方法中,最终的解析方法在47行:

builder.append(handler.handleToken(expression.toString()));

在这一行代码会调用TokenHandler这个类的handleToken方法,获取参数名对应的结果

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;
}

这个方法主要涉及几个操作

  1. 使用Ognl获取该参数名对应的值。

该结果值是直接使用String.valueOf进行解析,那么在这一步中,就有可能导致SQL注入的问题了。

  1. 检查结果是否有注入风险。

这个方法名checkInjection看起来就像是用于检查解析后的结果是否有注入SQL的风险的。但是呢,这个方法并不会起任何作用。因为这个方法起作用的前提是injectionFilter得不为null,但是在Mybatis中,并没有对这个属性进行任何的赋值行为,所以也就没有任何用处了。

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

解析例子

现在有一条使用了$号的SQL:

SELECT * FROM log WHERE content='${id}'

content哈哈哈时,经过Mybatis的解析后,会变成什么样呢?

SELECT * FROM log WHERE content='哈哈哈'

这样的SQL并没有任何问题,但是如果此时content的值为哈哈哈'; DROP TABLE log --的话,SQL解析后的结果就长这样了:

SELECT * FROM log WHERE content='哈哈哈'; DROP TABLE log --'

就会导致整个log表的数据被清除了,而这正是不当使用**$**的问题了。

#号占位符

既然$号有这么多的问题,为什么#号却不会有SQL注入的问题呢?我们来从实际例子来逐步展开。
现在有一个简单的SQL语句:

SELECT * FROM log WHERE content=#{id}

这个语句唯一不同的点就是将'${id}'换成了#{id},但是在Mybatis中的解析却是天差地别了。

初始化解析

$号不一样的是,在初始化Mybatis的MappedStatement时,检测到#号时,会提前初始化该SQL语句。无论是在注解中写SQL还是在Xml文件中写SQL,解析#号的方法最终都会进入到org.apache.ibatis.builder.SqlSourceBuilder#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;
    if (configuration.isShrinkWhitespacesInSql()) {
        sql = parser.parse(removeExtraWhitespaces(originalSql));
    } else {
        sql = parser.parse(originalSql);
    }
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
}

这个解析与$号的解析类似,也是由TokenHandler类进行参数名与对象之间的转换。
#号的替换中,则是ParameterMappingTokenHandler来进行参数名与对象之间的转换。
但是这个类的handleToken方法比较特别,返回值居然是一个**?**。并且在返回结果之前,还有一步操作

public String handleToken(String content) {
    parameterMappings.add(buildParameterMapping(content));
    return "?";
}

这个buildParameterMapping方法太长了,还是来看看具体返回了啥吧。
image.png
可以看到,这个方法的作用似乎是给SQL中的每一个占位符进行参数解析,将占位符对应的参数的类型、数据库类型、填充类型等都进行了解析。
这个初始化解析结束后,这一条SQL就变成了下面的样子了:

SELECT * FROM log WHERE content=?

并且还有一个集合parameterMappings装载了SQL中占位符的属性。

实际替换参数

初始化后,Mybatis在真正查询就会将利用PreparedStatement进行?占位符的替换了。

// org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters
public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    // 这个parameterMappings正是出事话解析SQL得到的参数映射集合
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        MetaObject metaObject = null;
        // 遍历每一个参数映射
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                // 获取参数对应的值
                Object value;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    if (metaObject == null) {
                        metaObject = configuration.newMetaObject(parameterObject);
                    }
                    value = metaObject.getValue(propertyName);
                }
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                if (value == null && jdbcType == null) {
                    jdbcType = configuration.getJdbcTypeForNull();
                }
                try {
                    // 设置每个?号对应的值
                    typeHandler.setParameter(ps, i + 1, value, jdbcType);
                } catch (TypeException | SQLException e) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                }
            }
        }
    }
}

typeHandler.setParameter这个方法则是利用了PreparedStatement类的方法,将?替换成传入的参数。
image.png
PreparedStatement在填充具体值会对参数进行转义,比如上述的SQL以及参数在查询时则会变成:

SELECT * FROM log WHERE content='哈哈哈''; DROP TABLE log --'

则不会有SQL注入的风险了。

总结

$号:直接替换占位符中的内容,在不对参数进行校验的情况下,易出现SQL注入问题。
#号:在预编译SQL的前提下,将参数名替换成?号,并利用PreparedStatement进行占位符的替换,在替换过程中,会对注入值进行转义避免SQL注入。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/783520.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

spark任务,使用 repartition 对数据进行了重新分区,但任务输入数据大小仍存在不均衡

目录 目录 确认 Spark 任务重新分区后的数据不均衡 1. 检查分区大小 2. 使用 DataFrame API 检查分区 3. 使用 Spark UI 查看分区情况 4. 使用日志记录分区信息 可能原因 1. 数据分布不均衡 2. 分区策略 3. 数据预处理 解决方案 1. 检查数据分布 2. 使用 coalesce…

Java反射与Fastjson的危险反序列化

什么是Java反射&#xff1f; 在前文中&#xff0c;我们有一行代码 Computer macBookPro JSON.parseObject(preReceive,Computer.class); 这行代码是什么意思呢&#xff1f;看起来好像就是我们声明了一个名为 macBookPro 的 Computer 类&#xff0c;它由 fastjson 的 parseObje…

工程仪器振弦采集仪的设计与研发进展

工程仪器振弦采集仪的设计与研发进展 工程仪器振弦采集仪是一种用于测量和记录物体振动参数的仪器。它能够实时采集物体的振动信号&#xff0c;并通过内部的传感器将振动信号转化为电信号&#xff0c;然后进行信号放大和处理&#xff0c;最终以数字形式显示或存储。 河北稳控科…

2024图纸加密软件TOP8排行丨企业保护数据安全最佳选择

图纸往往包含了设计人员的创意和智慧&#xff0c;是企业的重要资产。加密可以防止未经授权的复制、分发或使用&#xff0c;保护设计的原创性和独特性不被侵犯。 许多图纸可能含有公司的商业秘密&#xff0c;比如特定的技术参数、生产流程或是产品设计等。这些信息若泄露给竞争…

股票数据分析(K线图、均值图、MACD图、RSI图)--股票日数据

数据 数据是上证指数日行情数据&#xff0c;股票代码000002.sz&#xff0c;原始数据shdata示例如下&#xff1a; 读取数据&#xff1a; import numpy as np import pandas as pd import mplfinance as mpf import matplotlib.pyplot as plt from datetime import datetime imp…

JVM原理(二十):JVM虚拟机内存的三特性详解

1. 原子性、可进行、有序性 1.1. 原子性 Java内存模型围绕着在并发过程中如何处理原子性、可见性和有序性这三个特征来建立的。 Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write这六个。我们大致可以认为&#xff0c;基本数据类型的访问、…

vllm技术分享

vLLM Question&#xff1a; 推理所生成的序列长度大小是无法事先预知的&#xff0c;大部分框架会按照(batch_size, max_seq_len)这样的固定尺寸&#xff0c;在gpu显存上预先为一条请求开辟一块连续的矩形存储空间。这样的分配方法很容易引起“gpu显存利用不足”的问题&#xff…

ICE启动AI:人工智能高频交易平台测试进入尾段

Intercontinental Exchange, Inc.(ICE)宣布,其革命性AI高频交易平台ICE.AI已经完成搭建,目前已全面进入测试最终阶段,该平台利用先进的人工智能技术,主旨在提升交易效率和市场分析的精确度,即将为全球交易者带来前所未有的交易体验。 性能验证: ICE.AI平台在测试阶段主要进行性…

【QT中堆栈布局的实现】

学习分享 1、环境设置&#xff0c;头文件2、.h文件2.1、主界面.h文件2.2、对话界面1.h文件2.3、对话界面2.h文件 3、.cpp文件3.1、对话界面1.cpp3.2、对话界面2.cpp3.3、主界面.cpp3.4、main.cpp 1、环境设置&#xff0c;头文件 该示例使用C14实现&#xff0c;因此在QT项目pro文…

【银河麒麟】系统内存使用异常现象分析及建议

1.现象描述 问题机器系统内存占用长时间90%以上&#xff0c;同时伴随着高iowait&#xff0c;在故障时无法ssh登录&#xff0c;同时也影响生产业务。但之后系统内存占用会突然掉下来&#xff0c;在内存自己掉下来后能ssh登录。 2.显示分析 2.1 sa日志分析 查看问题机器3月15日…

什么是企业服务总线?它包含哪些技术组件?

我们每个人都会去医院&#xff0c;您描述下我们去医院的场景&#xff0c;然后引出这个挂号流程&#xff0c;通过挂号流程中的一个问题或者什么东西来吸引他的好奇心&#xff0c;这样呢&#xff1f;会比现在的预设场景好一些。我举个例子&#xff0c;人工智能怎么帮人看病。如果…

关于put提交不了参数的解决办法

html中form表单只支持GET与POST请求&#xff0c;而DELETE、PUT等method并不支持&#xff0c; 如图所示 参数请求改成RequestBody&#xff0c;用json格式传参即可解决问题

AI直播手机APP震撼发布!3大场景直播,60秒一键开播!

无需繁琐准备&#xff0c;无需复杂操作&#xff0c;60 秒在抖音及其他平台一键开播&#xff0c;青否数字人AI直播APP正式发布&#xff01; 3大AI直播类型&#xff0c;6大核心 AIGC 技术&#xff0c;让新手小白也能轻松搞定数字人在全平台直播&#xff0c;并且有效规避违规风险&…

数据跨境法案:美国篇上

近年来随着全球数字化的加速发展&#xff0c;数据已成为国家竞争力的重要基石。在这样的背景下&#xff0c;中国软件和技术出海的场景日益丰富。本系列邀请到在跨境数据方面的研究人员针对海外的数据跨境政策进行解读。 本期将针对美国对数据跨境流动的态度和政策进行阐释。过…

代码随想录算法训练营Day62|冗余连接、冗余连接II

冗余连接 108. 冗余连接 (kamacoder.com) 考虑使用并查集&#xff0c;逐次将s、t加入并查集中&#xff0c;当发现并查集中find(u)和find(v)相同时&#xff0c;输出u和v&#xff0c;表示删除的边即可。 #include <iostream> #include <vector> using namespace s…

游戏开黑语音-使用云服务器部署teamspeak服务(系统Ubuntu 20.04 LTS)

目录 前置物品服务器调整及部署1.重装系统2.换源3.下载teamspeak服务端并部署 连接服务器参考 前置物品 一台云服务器&#xff08;系统&#xff1a;Ubuntu 20.04 LTS) 服务器调整及部署 1.重装系统 在腾讯云官网的主机控制台内&#xff0c;选择重装系统 (由于之前为了快速和…

【收藏】欧盟CE、美国FDA法规及标准查询常用网站

01 CE法规&标准查询网站 医疗器械主管部门的网站 网址: https://www.camd-europe.eu/ 简介: CAMD的全称是Competent authorities for medical devices&#xff0c;翻译成中文叫做医疗器械监管机构&#xff0c;实际上它指的是欧盟成员国医疗器械监管机构的联盟&#xff…

江门数字化mes系统定制哪家好 珠海盈致mes系统服务商

对于江门数字化MES系统的定制服务&#xff0c;选择珠海盈致科技是一个不错的选择。珠海盈致科技是一家专业的智能制造解决方案提供商&#xff0c;具有丰富的数字化制造和MES系统定制经验。以下是选择珠海盈致科技的一些优势&#xff1a; 专业团队&#xff1a;珠海盈致科技拥有一…

Hack The Box-PermX

总体思路 CVE-2023-4220->敏感信息收集->符号链接攻击 信息收集&端口利用 nmap -sSVC permx.htbStarting Nmap 7.94SVN ( https://nmap.org ) at 2024-07-07 21:16 EDT Nmap scan report for permx.htb Host is up (0.24s latency). Not shown: 998 closed tcp po…

为什么要设计DTO类

为什么要使用DTO类&#xff0c;下面以新增员工接口为例来介绍。 新增员工 1.1 需求分析和设计 1.1.1 产品原型 一般在做需求分析时&#xff0c;往往都是对照着产品原型进行分析&#xff0c;因为产品原型比较直观&#xff0c;便于我们理解业务。 后台系统中可以管理员工信息…