V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
LeeReamond
V2EX  ›  问与答

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

  •  2
     
  •   LeeReamond · 2021-07-25 04:37:06 +08:00 · 3759 次点击
    这是一个创建于 1216 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

    23 条回复    2021-07-27 05:52:20 +08:00
    tinkerer
        1
    tinkerer  
       2021-07-25 05:13:10 +08:00
    SQL 有参数化执行,不只是拼字符串。

    https://www.w3schools.com/sql/sql_injection.asp
    CEBBCAT
        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
    LeeReamond
        3
    LeeReamond  
    OP
       2021-07-25 06:01:49 +08:00
    @tinkerer 问题问的就是参数化如何实现


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

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

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

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

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

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

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

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

    可以说 prepared statement 其实并不是防注入而发明的,而是为了更快(省去编译优化 SQL 的过程,可以多次复用)。只不过因为它的原理,它天然不会被注入而已。
    Jooooooooo
        19
    Jooooooooo  
       2021-07-25 16:43:18 +08:00
    传入只能是字符串, 不能是命令
    libook
        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 技术栈,写出来的程序也是会一样的烂。
    akira
        21
    akira  
       2021-07-25 23:47:55 +08:00
    客户端格式化是啥。。。你说的不会是 sql 代码格式化吧

    数据库服务器端一般会提供预编译和参数化执行,这 2 个功能“恰好”可以完成防注入的事情,和客户端没啥关系的
    whileFalse
        22
    whileFalse  
       2021-07-26 09:42:47 +08:00 via iPhone
    楼主知道怎么在双引号包裹的字符串中使用双引号吗?
    对,要使用斜杠转义。

    但是某些人不知道,所以就有了注入。
    SjwNo1
        23
    SjwNo1  
       2021-07-27 05:52:20 +08:00
    有些特殊字符是会绕过预编译的,例如 %
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5301 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 08:24 · PVG 16:24 · LAX 00:24 · JFK 03:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.