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。
修改配置(需要超级用户,context 为 superuser):
-- 立即生效,仅当前会话
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 怎么设,注入都不可能发生。
现代部署的建议
-
保持默认即可——
backslash_quote = safe_encoding+standard_conforming_strings = on是现代 PostgreSQL 的出厂设置,对 UTF-8 用户来说已经足够安全。不要为了"兼容老代码"把它改成on。 -
如果必须用 SJIS / GBK 编码——确认
backslash_quote在safe_encoding或off状态下运行。可以用以下命令快速验证:
psql -c "SHOW backslash_quote; SHOW server_encoding; SHOW client_encoding;"
-
迁移到 UTF-8——SJIS 和 GBK 的多字节歧义问题不止影响
backslash_quote,还涉及其他字符串处理边界。UTF-8 没有这些歧义,是更干净的选择。 -
代码层面用参数化查询——这是比任何 GUC 配置更可靠的防线。手动转义总会有遗漏的边界情况,参数化查询从设计上消除了整个问题类别。
-
审计遗留代码——如果你的项目中有
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 是一个因真实攻击而诞生的参数,它的存在本身就是一段安全史的提醒:编码与转义的交互比直觉更复杂,而最简单的防御,永远是不要自己去拼那个字符串。