sqle icon indicating copy to clipboard operation
sqle copied to clipboard

MySQL审核存储过程脚本时SQL片段切分不正确

Open sjjian opened this issue 9 months ago • 2 comments

版本信息(Version)

main

问题描述(Describe)

MySQL审核存储过程脚本时SQL片段切分不正确

截图或日志(Log)

输入的存储过程脚本如下:

DELIMITER $$

USE `fwdb`$$

DROP PROCEDURE IF EXISTS `_GS_GM_Check`$$

CREATE DEFINER=`root`@`%` PROCEDURE `_GS_GM_Check`(vi_uid INT,vi_pwd VARCHAR(32),vi_ip VARCHAR(100),OUT vo_level INT,OUT vo_code INT)
BEGIN
SET vo_level = 0;
	-- JinSQ 2016-04-08 CHECK IP ADD START
	IF IFNULL(vi_ip,' ')=' ' THEN
		BEGIN
	-- JinSQ 2016-04-08 CHECK IP ADD END
			-- if exists(select 1 from T_GM_Info where f_userid = vi_uid and f_pwd = vi_pwd and F_ip = vi_ip and F_Active = 1) 
			IF EXISTS(SELECT 1 FROM T_GM_Info WHERE f_userid = vi_uid AND F_Active = 1) 
			THEN
				-- select f_level into vo_level from T_GM_Info where f_userid = vi_uid AND f_pwd = vi_pwd AND F_ip = vi_ip AND F_Active = 1;
				SELECT f_level INTO vo_level FROM T_GM_Info WHERE f_userid = vi_uid AND F_Active = 1;
				SET vo_code = 1000;  -- success
			ELSE
				SET vo_code = 1001;  --  fail
			END IF;	
	-- JinSQ 2016-04-08 CHECK IP ADD START
		END;
	ELSE 
		BEGIN
			IF EXISTS(SELECT 1 FROM T_GM_Info WHERE f_userid = vi_uid AND F_ip = vi_ip AND F_Active = 1) 
			THEN
				SELECT f_level INTO vo_level FROM T_GM_Info WHERE f_userid = vi_uid AND F_ip = vi_ip AND F_Active = 1;
				SET vo_code = 1000;  -- success
			ELSE
				SET vo_code = 1001;  --  fail
			END IF;	
		END;
	END IF;
	-- JinSQ 2016-04-08 CHECK IP ADD END
    END$$

DELIMITER ;

审核后切分的片段如下:

DELIMITER $$

USE `fwdb`$$

DROP PROCEDURE IF EXISTS `_GS_GM_Check`$$

CREATE DEFINER=`root`@`%` PROCEDURE `_GS_GM_Check`(vi_uid INT,vi_pwd VARCHAR(32),vi_ip VARCHAR(100),OUT vo_level INT,OUT vo_code INT)
BEGIN
SET vo_level = 0;
	-- JinSQ 2016-04-08 CHECK IP ADD START
	IF IFNULL(vi_ip,' ')=' ' THEN
		BEGIN
	-- JinSQ 2016-04-08 CHECK IP ADD END
			-- if exists(select 1 from T_GM_Info where f_userid = vi_uid and f_pwd = vi_pwd and F_ip = vi_ip and F_Active = 1) 
			IF EXISTS(SELECT 1 FROM T_GM_Info WHERE f_userid = vi_uid AND F_Active = 1) 
			THEN
				-- select f_level into vo_level from T_GM_Info where f_userid = vi_uid AND f_pwd = vi_pwd AND F_ip = vi_ip AND F_Active = 1;
				SELECT f_level INTO vo_level FROM T_GM_Info WHERE f_userid = vi_uid AND F_Active = 1;
				SET vo_code = 1000;  -- success
			ELSE
				SET vo_code = 1001;  --  fail
			END IF;	
	-- JinSQ 2016-04-08 CHECK IP ADD START
		END;

	ELSE 
		BEGIN
			IF EXISTS(SELECT 1 FROM T_GM_Info WHERE f_userid = vi_uid AND F_ip = vi_ip AND F_Active = 1) 
			THEN
				SELECT f_level INTO vo_level FROM T_GM_Info WHERE f_userid = vi_uid AND F_ip = vi_ip AND F_Active = 1;
				SET vo_code = 1000;  -- success
			ELSE
				SET vo_code = 1001;  --  fail
			END IF;	
		END;

	END IF;

	-- JinSQ 2016-04-08 CHECK IP ADD END
    END$$

DELIMITER ;

如何复现(To Reproduce)

通过快捷审核随意创建一个MySQL的静态审核,输入上面的SQL脚步即可复现

实现方案

无法正确切分存储过程原因: 1、解析器对于一条存储过程语句的匹配规则是根据"BEGIN"和"END"关键字,但是复杂存储过程可以嵌套,解析器无法处理嵌套的情况(https://dev.mysql.com/doc/refman/5.7/en/begin-end.html) 2、如果一条存储过程定义中本身包含默认分隔符";",所以必须重新定义分隔符,需用到关键字delimiter,但是解析器无法解析该关键字。(https://dev.mysql.com/doc/refman/5.7/en/stored-programs-defining.html) 如:

mysql> delimiter //

mysql> CREATE PROCEDURE dorepeat(p1 INT)
    -> BEGIN
    ->   SET @x = 0;
    ->   REPEAT SET @x = @x + 1; UNTIL @x > p1 END REPEAT;
    -> END
    -> //
Query OK, 0 rows affected (0.00 sec)

mysql> delimiter ;

mysql> CALL dorepeat(1000);
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT @x;
+------+
| @x   |
+------+
| 1001 |
+------+
1 row in set (0.00 sec)

解决方案:

变更影响面

受影响的模块或功能

外部引用的潜在问题或风险

版本兼容性

测试建议

sjjian avatar May 07 '24 06:05 sjjian

实现方案

新增两个结构,delimiter,splitter,和一个接口block,对两个结构体parser和scanner新增了一些方法,给splitter调用。其中splitter拥有三个成员:delimiter,parser,scanner。具体职责如下:

  1. delimiter用于识别分隔符定义语法和sql文本片段中的分隔符
  2. parser是pincap的parser,用于将sql语句解析为抽象语法树节点
  3. scanner是pincap的scanner,用于扫描sql语句中的token
  4. splitter还会调用block用于检测识别内部包含多条SQL语句的BeginEnd语句块,以及其中的流程控制语句块。

工作流程:

  1. splitter会使用scanner遍历sql文本中的token
  2. 由splitter调用allblock识别BeginEnd语句块,splitter会跳过该语句块中分隔符的识别
  3. 由delimiter识别分隔符定义语法,变更当前上下文的分隔符,并且识别分隔符
  4. 由splitter基于识别到的分隔符,将SQL文本切分为一条条SQL
  5. 最后由parser将识别到的sql文本转化为抽象语法树节点,将不能识别为sql的文本转化为UnparsedStmt
  6. 输出时,不会输出定义分隔符的mysql客户端语法,并且会把自定义的分隔符替换为默认分隔符

测试影响面

影响所有SQL审核相关的功能 影响所有涉及解析SQL文本的功能

兼容性分析

  • 不涉及修改model层,只修改driver,因此兼容新旧版本数据
  • 兼容其他调用parse的函数和方法,原因:
  1. 相比之前的parse方法,在解析SQL之前先根据分隔符切分SQL,当正确使用分隔符时,切分结果为一条条的SQL语句(对于使用自定义分隔符分隔的sql,分隔符会被替换为默认分隔符;
  2. 在此基础上,对一条条SQL进行解析,对于能够解析成功的SQL,结果与之前的parse方法相同,对于不能够解析成功的SQL,解析结果为为解析的节点,但与之前不同,新版的结果在正确使用分隔符的前提下,一定是一条SQL,而不会有多条SQL杂糅在一起。因此也能够单独执行这条SQL。
  3. 对于Begin.....End语句块,在不支持解析分隔符之前,会被错误的拆分为多条SQL,在支持解析分隔符之后,可以完整的识别为一条SQL,当然也存在一种特定情况,即出现没有定义分隔符,且只有这一条Begin...End的SQL语句,我们通过检测Begin...End并跳过中间的分隔符,来达到使该语句块能够正确切分为一条SQL的目的。

winfredLIN avatar Jul 01 '24 08:07 winfredLIN

测试

测试使用了三个文件,前两个使用的是文件模式,最后一个使用的是SQL模式。测试SQL由AI生成, 基础Prompt:

对于MySQL,请帮助我写一些SQL,必须满足:
1. 尽量包含各种类型的SQL语句
2. 其中有一些包含带有BEGIN END的语句块,这些语句块需要使用自定义分隔符来分隔
3. 由于这些数据是用于测试,在MySQL上运行完毕之后数据需要清除,因此请您在创建表、视图、存储过程等对象后,在SQL文本的最后,请给出相应的删除语句。
感谢

测试文件: test_db.employees.txt test_db.projects.txt test_db.department.txt

测试结果截图

测试执行的文件均能执行成功,符合预期

  • 使用文件模式 screencapture-10-186-62-32-9999-sqle-project-700300-exec-workflow-66-files-6-sqls-2024-07-04-14_13_27 screencapture-10-186-62-32-9999-sqle-project-700300-exec-workflow-65-files-5-sqls-2024-07-04-14_10_48
  • 使用SQL模式 screencapture-10-186-62-32-9999-sqle-project-700300-exec-workflow-1808780996166815744-2024-07-04-16_33_21

winfredLIN avatar Jul 04 '24 06:07 winfredLIN