一个疑问是,各个语言的 sql 客户端为了防止 sql 注入都有格式化功能,它的底层实现是用最基础的字符串转义来实现的吗?单纯靠转义能覆盖复杂的语义的所有情况吗?
1
tinkerer 2021-07-25 05:13:10 +08:00
|
2
CEBBCAT 2021-07-25 05:19:35 +08:00 via Android 1
你这个问题就不太对,防止 SQL 注入最直观的方法是使用 PREPARE 来预编译 SQL 语句,这样语义就群定下来了。
接下来才是话说回来,有的时候为了提高数据库 IO 会采用本地 SQL 格式化功能,也就是 SQL 客户端自己做字符串格式化,替换“?”占位符。关于它的实现我不是很了解,你可以看看 github.com/go-sql-driver/mysql 的 interpolateParams 参数( https://github.com/go-sql-driver/mysql/blob/bcc459a906419e2890a50fc2c99ea6dd927a88f2/connection.go#L198 ),我看大体上就是格式化。 关于你问的是不是可能有漏洞,那肯定有,上面列的库就提示了这个问题: https://stackoverflow.com/a/12118602 |
3
LeeReamond OP |
4
CEBBCAT 2021-07-25 06:26:37 +08:00 via Android
|
5
eason1874 2021-07-25 07:15:10 +08:00 5
先明确 SQL 注入原理,就能理解预防原理。
SQL 注入依赖两个前提:1 、通过字符串拼接的方式生成 SQL 命令; 2 、未对用户输入字符进行过滤或转义。所以用户可以按照 SQL 命令语法去提交输入,使输入称为 SQL 命令的一部分,被程序拼接成了有效的 SQL 命令并运行。 预防 SQL 注入,以前是流行过滤和转义,就是防止被当成 SQL 命令去解释,这是程序实现的。 现在流行参数化,参数化是数据库实现的,数据库先把程序的 SQL 命令解释完成,后面输入参数就不再解释了,只当作参数传入,不会成为 SQL 命令的一部分,所以就算没转义也不会导致恶意输入被当成 SQL 命令执行。 |
6
crab 2021-07-25 09:18:44 +08:00
|
7
passerbytiny 2021-07-25 09:56:41 +08:00 via Android
3 楼(我看到的)解释的很清楚了。SQL 注入的必要条件之一是“外面传入的字符串,参与了待执行 SQL 语句的拼接过程”,参数化或者预编译,将 SQL 拼接限制在服务器程序内部,使得该必要条件不成立,从而避免了 SQL 注入。
不过你要知道,早期的(我不确定是否是最早) JDBC 预编译,它的设计目的是性能优化和编码优化,防止 SQL 注入只是个副作用。 回到楼主的疑问,你所说的是客户端防注入手段,其实是一种“客户端参数化”或者“SQL 拼接过程参数化”,有效但不完全有效,会有漏网的。这跟通常所说的参数化不一样,后者的本质是预编译,是要数据库本身参与验证的,能够 100 %防止 SQL 注入。 |
8
JJsty1e 2021-07-25 10:34:08 +08:00 via iPhone
顺便也问一个问题,如果用预处理语句,客户端应该是要发两个请求到 mysql server,为什么不设计成 预处理语句+参数一起发送呢?这样不就减少一次请求,加快 mysql 执行效率了吗
|
9
potatowish 2021-07-25 10:48:29 +08:00 via iPhone
@JJsty1e 我也想问,这两个步骤为什么是在客户端分两步执行,而不是数据库端拆分成两步执行
|
10
gam2046 2021-07-25 10:57:14 +08:00 1
历史上对抗 SQL 注入的方案有好多,对输入参数过滤、转义,参数化查询、存储过程、使用数据库视图等等,中心思想都是一样的,使得用户输入的参数部分,不能解释为 SQL 关键字
|
11
passerbytiny 2021-07-25 11:10:59 +08:00 via Android 1
@JJsty1e
@potatowish 首先,在损耗上,一个连接上发送两次数据跟发送一次数据几乎没区别。建立两次连接跟建立一次连接相比才有明显的时间损耗。预编译跟最终执行使用的是一个连接。至于效率,预编译更高,因为:一、就算你一次性提交上去,数据库服务器也要分成编译 /解释、执行两个阶段去处理,并不会提高效率;二、预编译可以被复用。 |
12
passerbytiny 2021-07-25 11:22:56 +08:00 via Android
|
13
ipwx 2021-07-25 12:13:01 +08:00
|
14
ipwx 2021-07-25 12:14:02 +08:00
@LeeReamond 举个例子,Python 标准库操作 Sqlite
cur.execute("select * from lang where first_appeared=:year", {"year": 1972}) 这里 :year 就是绑定参数,后面的字典给参数。 |
15
LeeReamond OP @ipwx 楼上有问参数化是什么意思的,大概就是这个意思,我好奇这个底层怎么实现的,是单纯的字符串转义吗。
|
16
iseki 2021-07-25 14:33:23 +08:00 via Android
这种类型的“参数化”大多数都是转换成数据库本身支持的“参数 sql”(prepareStatment 那种)+参数,然后提交给数据库解析
|
17
ipwx 2021-07-25 16:13:42 +08:00
@LeeReamond 不是。
这些数据库服务器或者嵌入式数据库都有更底层的协议的(不是纯粹的 SQL )。那些协议可以把带参数的 SQL (或者干脆预编译成字节码)和参数本身分开来打包传送给服务器(或者给嵌入式数据库的核心 API )。数据库系统是直接拿着 SQL 或者字节码 + 这些参数工作的。 譬如 PostgreSQL https://www.postgresql.org/docs/9.3/protocol-flow.html#PROTOCOL-FLOW-EXT-QUERY 先发送 SQL,PostgreSQL 解析并编译这个 SQL,然后发送参数,最后执行。编译过的 SQL 可以不用再发送一遍,直接发送新的参数,可以继续执行。 |
18
ipwx 2021-07-25 16:16:57 +08:00
顺便发送参数也不是变成字符串。你可以翻一翻各个数据库自己的文档,肯定是有二进制协议的。这很显然,数字用数字的格式发送,字符串用字符串的格式发送,全程不会混淆,自然不会被注入。
可以说 prepared statement 其实并不是防注入而发明的,而是为了更快(省去编译优化 SQL 的过程,可以多次复用)。只不过因为它的原理,它天然不会被注入而已。 |
19
Jooooooooo 2021-07-25 16:43:18 +08:00
传入只能是字符串, 不能是命令
|
20
libook 2021-07-25 20:21:38 +08:00
SQL 语句包含指令和参数两部分,注入问题存在的根本原因在于 SQL 拼接可能会导致改变原有指令,使得 SQL 做了预期之外的事情。
所以解决 SQL 注入问题的核心思想是确保传入的数据仅被用作参数,而不是被当作指令而改变了原意。 转义只是避免本应该是参数的输入数据误被解读为指令的手段之一,还有很多更先进的方案,其他楼层也都提到了,比如 command execution functions 、string functions 、parameterized query (这个比较广泛使用)、prepared statements 。 因为 SQL 本身语法上的特点,仅靠转义,可能难以覆盖所有的情况,所以基本上都是凭经验来尽可能规避。有这么一句话,就是烂程序员无论用什么牛 X 技术栈,写出来的程序也是会一样的烂。 |
21
akira 2021-07-25 23:47:55 +08:00
客户端格式化是啥。。。你说的不会是 sql 代码格式化吧
数据库服务器端一般会提供预编译和参数化执行,这 2 个功能“恰好”可以完成防注入的事情,和客户端没啥关系的 |
22
whileFalse 2021-07-26 09:42:47 +08:00 via iPhone
楼主知道怎么在双引号包裹的字符串中使用双引号吗?
对,要使用斜杠转义。 但是某些人不知道,所以就有了注入。 |
23
SjwNo1 2021-07-27 05:52:20 +08:00
有些特殊字符是会绕过预编译的,例如 %
|