PostgreSQL 的 SQL 注入攻击面与 MySQL 或 SQL Server 存在本质区别。很多人以为换个数据库后端,把单引号过滤了就万事大吉,但在 PostgreSQL 面前,这套偷懒的做法往往失效。PostgreSQL 拥有极其丰富的 SQL 语法、过程语言支持和内建函数,这些强大的特性一旦被注入者利用,攻击路径会变得异常刁钻。我们直接切入核心,看看在 PostgreSQL 环境下,注入到底是怎么发生的,以及如何从代码层到架构层逐级布防。

堆叠查询:多语句执行的真正威胁

在 MySQL 中,默认情况下 PHP 的 mysqli_query 不支持多语句执行,这在一定程度上限制了注入的危害。但 PostgreSQL 的原生驱动(如 libpq 和 Npgsql)天然支持多语句查询。也就是说,攻击者只要找到一个注入点,往往可以直接通过分号拼接任意 SQL 语句,不需要费心去闭合原查询。

比如后端代码拼接出这样的查询:

SELECT id, name FROM users WHERE email = 'user@example.com';

如果用户输入是:

user@example.com'; DROP TABLE users; --

最终发送给 PostgreSQL 的就是:

SELECT id, name FROM users WHERE email = 'user@example.com'; DROP TABLE users; --';

PostgreSQL 会老老实实执行两条语句,第二条直接删表。这不是危言耸听,很多内部管理系统、报表平台的查询接口就是这么被打穿的。因此,在 PostgreSQL 环境下,仅靠过滤单引号或关键字完全不够,必须从驱动层就杜绝拼接。

利用 COPY 指令实现文件级读写

PostgreSQL 的 COPY 指令是一个极其高效的数据导入导出工具,但在注入场景下,它变成了读写服务器文件的利器。如果数据库账号拥有超级用户权限,攻击者可以通过 COPY 把整个表导出到客户端可访问的文件系统,或者从服务器文件系统读取文件内容插入表中。

举个例子,攻击者在注入点执行:

COPY users TO '/tmp/users.csv' WITH CSV HEADER;

就能把用户表完整导出。更危险的是反过来:

COPY users FROM '/etc/passwd';

虽然这需要特定权限且受文件权限限制,但在配置不当的环境下,敏感文件泄露风险极高。即使数据库账号不是超级用户,如果云环境或容器挂载了敏感目录,COPY 仍然可能被滥用。防护的关键在于严格限制数据库账号权限,禁止业务账号使用 COPY TO/FROM PROGRAM 这类高危变体,并在 PostgreSQL 配置文件中合理设置 secure_file_priv 相关参数。

大规模数据提取:string_agg 与 array_agg 的妙用

传统注入手法中,攻击者常用 UNION SELECT 逐列猜解数据。但在 PostgreSQL 里,利用 string_agg 或 array_agg 聚合函数,一次请求就能把整张表的数据打包带走,极大地提高了数据窃取效率。

假设注入点返回的结果集会直接显示在页面上,攻击者可以构造:

' UNION SELECT string_agg(username || ':' || password, ';') FROM pg_shadow --

如果权限足够,这条语句会把所有数据库用户的哈希密码串成一个字符串返回。对于普通业务表,攻击者同样可以用一行查询把全表数据拼接成 JSON 或 CSV 格式输出。这种手法让原本需要数百次请求的盲注过程,压缩到一次完成,对日志监控和频率限制是极大的挑战。防御端必须确保应用层对查询结果集做严格的类型和结构校验,不要让原始错误信息或未处理的结果直接回显。

利用 PostgreSQL 特有无文件攻击:大对象与 lo_import

PostgreSQL 的大对象功能允许在数据库内部存储二进制文件。攻击者可以通过 lo_import 函数将服务器上的任意文件加载为大对象,再通过 lo_export 导出到可写目录,或者直接读取大对象内容返回给客户端。这种攻击方式不需要借助 Web 服务器的文件上传漏洞,完全在数据库层面完成文件传输。

攻击流程大致如下:

SELECT lo_import('/etc/passwd');

获取到大对象的 OID 后,再通过 loread 等函数逐步读取内容。如果应用存在 SQL 注入且数据库账号权限过高,攻击者就能以数据库进程的身份读取文件系统,甚至结合 COPY PROGRAM 执行操作系统命令。防御这类攻击,必须回收业务账号的大对象操作权限,并在 pg_hba.conf 中严格控制连接来源。

盲注加速:PostgreSQL 的条件表达式与错误利用

当应用不直接返回查询结果时,攻击者会转向盲注。PostgreSQL 提供了极其灵活的条件表达式,让盲注的探测速度远超其他数据库。比如利用 CASE WHEN 结合延时函数 pg_sleep,或者利用类型转换错误制造可观测的异常。

典型的延时盲注载荷:

' AND (SELECT CASE WHEN (current_setting('is_superuser')='on') THEN pg_sleep(5) ELSE pg_sleep(0) END) --

如果应用响应时间增加了 5 秒,攻击者就知道当前数据库用户是超级用户。更隐蔽的做法是利用除零错误或类型转换错误,通过应用是否返回 500 状态码来判断条件真假。PostgreSQL 的错误信息通常包含丰富的上下文,甚至可能泄露表名、列名等元数据。防御上,应用必须统一捕获数据库异常,返回通用的错误页面,绝不能把数据库报错直接抛给前端。同时,在数据库层面可以设置 statement_timeout 来限制单条语句的执行时间,削弱延时注入的效果。

二次注入与编码绕过:PostgreSQL 的字符串处理特性

PostgreSQL 对字符串的处理非常灵活,支持多种转义语法和 Unicode 编码。攻击者常利用 CHR 函数拼接字符串来绕过关键字黑名单过滤。例如,要构造 SELECT,可以写成:

CHR(83)||CHR(69)||CHR(76)||CHR(69)||CHR(67)||CHR(84)

这种纯 ASCII 码拼接的方式,让基于正则的关键字过滤形同虚设。另外,PostgreSQL 还支持美元符号引用的字符串常量,比如 $$ 或 $tag$,攻击者可以用它来绕过对单引号的转义和过滤。二次注入则更为隐蔽,攻击者先将恶意载荷存入数据库,比如在注册用户名时写入拼接语句,后续某个功能点读取该用户名并直接拼入另一条 SQL 时触发。这类漏洞的根因始终是拼接,而非输入过滤不严。

参数化查询与预编译:唯一的正解

面对 PostgreSQL 如此丰富的攻击面,最有效且成本最低的防护手段就是参数化查询。无论使用哪种编程语言,都应该使用 PostgreSQL 驱动提供的预编译语句和参数绑定功能。以 Java 的 PreparedStatement 为例:

String query = "SELECT id, name FROM users WHERE email = ?";
PreparedStatement pstmt = conn.prepareStatement(query);
pstmt.setString(1, userInput);
ResultSet rs = pstmt.executeQuery();

参数化查询从协议层面把 SQL 逻辑和数据彻底分离,攻击者输入的任何内容都只会被当作字符串值,不可能改变查询的语法树。对于动态表名、列名或 ORDER BY 这类无法参数化的场景,必须使用白名单校验。将允许的表名和列名预先定义在集合中,用户传入的值必须严格匹配白名单才允许拼入查询。任何试图通过黑名单过滤或转义来处理这些动态标识符的做法,最终都会留下绕过的缝隙。

纵深防御:权限最小化与数据库配置加固

即使代码层做到了参数化,也不意味着可以高枕无忧。PostgreSQL 的权限体系必须严格遵循最小化原则。业务账号不应该拥有创建函数、执行 COPY、访问系统表等高权限。通过 REVOKE 语句回收不必要的权限:

REVOKE ALL ON SCHEMA public FROM app_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;

同时,合理配置 pg_hba.conf,只允许应用服务器的 IP 地址连接数据库,禁止外部直接访问。在 postgresql.conf 中,限制连接数、启用日志审计、设置 statement_timeout 都能有效提升攻击难度。对于敏感数据,还可以结合 PostgreSQL 的列级加密扩展 pgcrypto,即使注入成功,拿到的也是密文。

运行时检测与虚拟补丁

在无法立即修复代码的历史遗留系统中,可以在数据库层部署运行时检测机制。利用 PostgreSQL 的审计扩展,如 pgaudit,记录所有 DDL 和 DML 操作,通过分析日志发现异常的堆叠查询或系统表访问。更主动的方案是使用数据库防火墙或反向代理,在流量到达数据库之前,对 SQL 语句进行语法分析和风险评分,阻断明显带有注入特征的请求。这种虚拟补丁的思路,可以为代码修复争取宝贵的时间窗口。

PostgreSQL 的注入防护不是某个单点技巧就能解决的问题。它的特性决定了攻击者一旦找到注入点,横向移动和权限提升的手段会非常丰富。从代码层的参数化查询,到数据库层的权限收敛和审计,再到网络层的访问控制,每一层都在降低被彻底攻破的概率。把这些措施落实到位,远比在应用代码里写一堆脆弱的关键字过滤要可靠得多。