PostgreSQL 的 backslash_quote:一个因多字节编码 SQL 注入而诞生的 GUC

2026-05-14 38 预计阅读时间:1 分钟
来源:postgr.es AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

2006 年,PostgreSQL 修复了一个与多字节字符编码相关的 SQL 注入漏洞。修复的产物之一是 backslash_quote——一个至今仍留在 PostgreSQL 中的 GUC 参数,专门控制反斜杠是否能用于转义单引号。它看起来像个冷门配置,但理解它的来龙去脉,能让你对字符串转义和编码安全有更深的把握。

漏洞是怎么发生的

问题出在多字节编码(如 SJIS)与反斜杠转义的交互上。

在 SJIS 等编码中,某些字符的第二个字节恰好是 0x5c——也就是 ASCII 反斜杠 \ 的值。比如 SJIS 编码的「表」字,字节序列是 0x95 0x5c

当应用层用反斜杠转义用户输入中的单引号时,数据库收到的字节流可能是这样的:

输入: 表'  →  SJIS字节: 0x95 0x5c 0x27
应用层转义后: 表\'  →  SJIS字节: 0x95 0x5c 0x27

数据库按 SJIS 解析时,0x95 0x5c 被识别为一个完整字符「表」,0x5c 不再是反斜杠——它被"吃掉"成了多字节字符的一部分。于是 0x27(单引号)裸露出来,字符串边界被打破,注入成功。

这不是理论推演,而是真实被报告的 CVE。受影响的编码包括 SJIS、BIG5、GBK 等多种 CJK 多字节编码。

backslash_quote 的作用

backslash_quote 直接从根源上切断这个攻击面:当它设为 off,PostgreSQL 禁止用反斜杠 \ 转义单引号,字符串内的单引号只能用 SQL 标准的 ''(双单引号)方式转义。

-- backslash_quote = on(传统行为)
SELECT 'It\'s fine';  -- 反斜杠转义单引号,合法

-- backslash_quote = off
SELECT 'It\'s fine';  -- 报错:反斜杠不能转义单引号
SELECT 'It''s fine';  -- 正确:SQL 标准双单引号转义

没了反斜杠转义单引号这条路,多字节编码"吞掉反斜杠"的注入手法就彻底失效了。

参数有三个合法值:

行为
on 允许反斜杠转义单引号(传统行为,有注入风险)
off 禁止反斜杠转义单引号(安全)
safe_encoding 仅当当前编码不会产生歧义时才允许(默认值)

safe_encoding 是默认值,也是最实用的选择:在 UTF-8 等不存在"第二字节等于 0x5c"问题的编码下,它等价于 on;在 SJIS 等有风险的编码下,它等价于 off

检查与配置:实际操作

查看当前值和编码:

-- 查看 backslash_quote 当前设置
SHOW backslash_quote;

-- 查看服务器编码
SHOW server_encoding;

-- 两者一起看
SELECT name, setting, context, short_desc
  FROM pg_settings
 WHERE name IN ('backslash_quote', 'standard_conforming_strings', 'server_encoding');

standard_conforming_strings 是另一个相关 GUC。当它为 on(PostgreSQL 9.1 之后的默认值),普通字符串字面量按 SQL 标准处理,反斜杠就是普通反斜杠,不是转义符。只有 E'...' 才走传统转义。两个参数协同工作:

  • standard_conforming_strings = on + backslash_quote = safe_encoding:日常最安全的组合,也是现代 PostgreSQL 的默认状态。
  • 如果你还在用 SJIS / GBK 编码连接数据库,务必确认 backslash_quote 不是 on

修改配置(需要超级用户,contextsuperuser):

-- 立即生效,仅当前会话
SET backslash_quote = off;

-- 写入 postgresql.conf,重启后生效
-- 也可以在 postgresql.conf 中直接编辑
ALTER SYSTEM SET backslash_quote = off;
SELECT pg_reload_conf();

用参数化查询彻底绕过转义问题

配置 GUC 是防御层,但最根本的做法是不要手动拼接字符串。参数化查询让转义规则与你无关:

import psycopg2

conn = psycopg2.connect(
    dbname="mydb",
    # 即使客户端编码是 SJIS,参数化查询也不受 backslash_quote 影响
    options="-c client_encoding=SJIS"
)
cur = conn.cursor()

# 安全:参数化查询,驱动层处理转义
cur.execute("SELECT * FROM users WHERE name = %s", ("表'",))

# 危险:手动拼接 + 反斜杠转义——backslash_quote 的漏洞场景
name = "表'"
escaped = name.replace("'", "\\'")  # 这就是 2006 年漏洞的触发方式
cur.execute(f"SELECT * FROM users WHERE name = '{escaped}'")  # 不要这样做

参数化查询把值和 SQL 语法彻底分离,无论编码是什么、backslash_quote 怎么设,注入都不可能发生。

现代部署的建议

  1. 保持默认即可——backslash_quote = safe_encoding + standard_conforming_strings = on 是现代 PostgreSQL 的出厂设置,对 UTF-8 用户来说已经足够安全。不要为了"兼容老代码"把它改成 on

  2. 如果必须用 SJIS / GBK 编码——确认 backslash_quotesafe_encodingoff 状态下运行。可以用以下命令快速验证:

psql -c "SHOW backslash_quote; SHOW server_encoding; SHOW client_encoding;"
  1. 迁移到 UTF-8——SJIS 和 GBK 的多字节歧义问题不止影响 backslash_quote,还涉及其他字符串处理边界。UTF-8 没有这些歧义,是更干净的选择。

  2. 代码层面用参数化查询——这是比任何 GUC 配置更可靠的防线。手动转义总会有遗漏的边界情况,参数化查询从设计上消除了整个问题类别。

  3. 审计遗留代码——如果你的项目中有 E'...' 字面量或手动 replace("'", "\\'") 的拼接,这就是需要改造的地方:

-- 旧代码:依赖反斜杠转义
INSERT INTO logs (msg) VALUES (E'It\'s done');

-- 改写:SQL 标准双单引号
INSERT INTO logs (msg) VALUES ('It''s done');

-- 更好:应用层参数化
-- cur.execute("INSERT INTO logs (msg) VALUES (%s)", ("It's done",))

backslash_quote 是一个因真实攻击而诞生的参数,它的存在本身就是一段安全史的提醒:编码与转义的交互比直觉更复杂,而最简单的防御,永远是不要自己去拼那个字符串。


相关推荐