防止 sql 注入的原理是什么?

2021-07-25 04:37:06 +08:00
 LeeReamond

一个疑问是,各个语言的 sql 客户端为了防止 sql 注入都有格式化功能,它的底层实现是用最基础的字符串转义来实现的吗?单纯靠转义能覆盖复杂的语义的所有情况吗?

3792 次点击
所在节点    问与答
23 条回复
tinkerer
2021-07-25 05:13:10 +08:00
SQL 有参数化执行,不只是拼字符串。

https://www.w3schools.com/sql/sql_injection.asp
CEBBCAT
2021-07-25 05:19:35 +08:00
你这个问题就不太对,防止 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
LeeReamond
2021-07-25 06:01:49 +08:00
@tinkerer 问题问的就是参数化如何实现


@CEBBCAT 预编译是生产级防注入办法,不过很多时候我们也不会预编译,而是直接依赖参数化实现防注入,因为这么写起来更快,我只是好奇这个是怎么实现的
CEBBCAT
2021-07-25 06:26:37 +08:00
@LeeReamond 你说的参数化是什么意思?可以定义一下这个名词吗?

另外我有一点怀疑你需要实现方式我已经贴给你了,假如你想问具体到某个语言的某个库函数,最好直接点出来
eason1874
2021-07-25 07:15:10 +08:00
先明确 SQL 注入原理,就能理解预防原理。

SQL 注入依赖两个前提:1 、通过字符串拼接的方式生成 SQL 命令; 2 、未对用户输入字符进行过滤或转义。所以用户可以按照 SQL 命令语法去提交输入,使输入称为 SQL 命令的一部分,被程序拼接成了有效的 SQL 命令并运行。

预防 SQL 注入,以前是流行过滤和转义,就是防止被当成 SQL 命令去解释,这是程序实现的。

现在流行参数化,参数化是数据库实现的,数据库先把程序的 SQL 命令解释完成,后面输入参数就不再解释了,只当作参数传入,不会成为 SQL 命令的一部分,所以就算没转义也不会导致恶意输入被当成 SQL 命令执行。
crab
2021-07-25 09:18:44 +08:00
passerbytiny
2021-07-25 09:56:41 +08:00
3 楼(我看到的)解释的很清楚了。SQL 注入的必要条件之一是“外面传入的字符串,参与了待执行 SQL 语句的拼接过程”,参数化或者预编译,将 SQL 拼接限制在服务器程序内部,使得该必要条件不成立,从而避免了 SQL 注入。

不过你要知道,早期的(我不确定是否是最早) JDBC 预编译,它的设计目的是性能优化和编码优化,防止 SQL 注入只是个副作用。

回到楼主的疑问,你所说的是客户端防注入手段,其实是一种“客户端参数化”或者“SQL 拼接过程参数化”,有效但不完全有效,会有漏网的。这跟通常所说的参数化不一样,后者的本质是预编译,是要数据库本身参与验证的,能够 100 %防止 SQL 注入。
JJsty1e
2021-07-25 10:34:08 +08:00
顺便也问一个问题,如果用预处理语句,客户端应该是要发两个请求到 mysql server,为什么不设计成 预处理语句+参数一起发送呢?这样不就减少一次请求,加快 mysql 执行效率了吗
potatowish
2021-07-25 10:48:29 +08:00
@JJsty1e 我也想问,这两个步骤为什么是在客户端分两步执行,而不是数据库端拆分成两步执行
gam2046
2021-07-25 10:57:14 +08:00
历史上对抗 SQL 注入的方案有好多,对输入参数过滤、转义,参数化查询、存储过程、使用数据库视图等等,中心思想都是一样的,使得用户输入的参数部分,不能解释为 SQL 关键字
passerbytiny
2021-07-25 11:10:59 +08:00
@JJsty1e
@potatowish
首先,在损耗上,一个连接上发送两次数据跟发送一次数据几乎没区别。建立两次连接跟建立一次连接相比才有明显的时间损耗。预编译跟最终执行使用的是一个连接。至于效率,预编译更高,因为:一、就算你一次性提交上去,数据库服务器也要分成编译 /解释、执行两个阶段去处理,并不会提高效率;二、预编译可以被复用。
passerbytiny
2021-07-25 11:22:56 +08:00
@JJsty1e
@potatowish
第三,就算不复用,如果预编译过程是异步的,那么“异步预编译—设置参数—提交执行”的过程的总执行时间,会少于“客户端参数化拼接 SQL—提交执行”过程。
ipwx
2021-07-25 12:13:01 +08:00
@LeeReamond 为啥我觉得预编译写起来更快 2333

大概是 C++/Python 的库都比较好用?
ipwx
2021-07-25 12:14:02 +08:00
@LeeReamond 举个例子,Python 标准库操作 Sqlite

cur.execute("select * from lang where first_appeared=:year", {"year": 1972})

这里 :year 就是绑定参数,后面的字典给参数。
LeeReamond
2021-07-25 13:48:26 +08:00
@ipwx 楼上有问参数化是什么意思的,大概就是这个意思,我好奇这个底层怎么实现的,是单纯的字符串转义吗。
iseki
2021-07-25 14:33:23 +08:00
这种类型的“参数化”大多数都是转换成数据库本身支持的“参数 sql”(prepareStatment 那种)+参数,然后提交给数据库解析
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 可以不用再发送一遍,直接发送新的参数,可以继续执行。
ipwx
2021-07-25 16:16:57 +08:00
顺便发送参数也不是变成字符串。你可以翻一翻各个数据库自己的文档,肯定是有二进制协议的。这很显然,数字用数字的格式发送,字符串用字符串的格式发送,全程不会混淆,自然不会被注入。

可以说 prepared statement 其实并不是防注入而发明的,而是为了更快(省去编译优化 SQL 的过程,可以多次复用)。只不过因为它的原理,它天然不会被注入而已。
Jooooooooo
2021-07-25 16:43:18 +08:00
传入只能是字符串, 不能是命令
libook
2021-07-25 20:21:38 +08:00
SQL 语句包含指令和参数两部分,注入问题存在的根本原因在于 SQL 拼接可能会导致改变原有指令,使得 SQL 做了预期之外的事情。

所以解决 SQL 注入问题的核心思想是确保传入的数据仅被用作参数,而不是被当作指令而改变了原意。

转义只是避免本应该是参数的输入数据误被解读为指令的手段之一,还有很多更先进的方案,其他楼层也都提到了,比如 command execution functions 、string functions 、parameterized query (这个比较广泛使用)、prepared statements 。

因为 SQL 本身语法上的特点,仅靠转义,可能难以覆盖所有的情况,所以基本上都是凭经验来尽可能规避。有这么一句话,就是烂程序员无论用什么牛 X 技术栈,写出来的程序也是会一样的烂。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/791577

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX