set search_path被注入篡改后,后续所有未显式指定schema前缀的查询将落入错误租户,导致租户隔离彻底失效;因其为会话级变量,污染后整个连接生命周期内均生效,且连接池复用会放大风险。

SET search_path 被注入篡改后会怎样
PostgreSQL 中 SET search_path TO tenant_a 是会话级操作,本身不防注入;一旦攻击者在参数里塞入 ; SET search_path TO tenant_b,后续所有未显式带 Schema 前缀的查询(如 SELECT * FROM users)就会落到错误租户下。这不是“可能漏查”,而是整条连接生命周期内默认 Schema 已被劫持。
常见高危点:
-
ORDER BY、GROUP BY子句里拼接字段名,极易被注入触发SET语句 - 连接池复用时,旧会话残留的
search_path可能污染新租户请求 - MySQL 没有
search_path,但类似风险出现在USE tenant_x+ 未限定库名的查询中
如何让 Schema 切换不被绕过
不能依赖 ORM 拦截器或中间件自动切换——它们对原生 SQL、@SelectProvider、<script></script> 标签完全失效。必须从执行源头卡死。
关键动作:
- 所有 SQL 必须显式带上 Schema 前缀:
SELECT * FROM tenant_a.users,禁用裸表名写法 - 数据库账号权限严格限制:只授予对应 Schema 的
USAGE和SELECT/INSERT/UPDATE,禁止CREATE和跨 Schema 权限 - 连接串中固定
database=tenant_x(MySQL)或用currentSchema=tenant_x(PostgreSQL JDBC),比运行时USE或SET更可靠 - 禁用客户端任意设置
search_path:DBA 执行ALTER ROLE app_user SET search_path = 'tenant_a';并REVOKE SET ON PARAMETER search_path FROM PUBLIC;
为什么 @DS 注解救不了 Schema 注入
@DS("tenant_a") 这类数据源路由注解只控制连接从哪个物理库/Schema 建立,不参与 SQL 构造。如果 DAO 层还用 String.format("SELECT * FROM users WHERE id = %s", id),哪怕路由对了,SQL 本身仍可被注入篡改 search_path。
更隐蔽的问题:
-
@Async或定时任务中,@DS默认不继承主线程上下文,可能路由到默认 Schema - MyBatis 的
<if test="name != null"></if>动态 SQL 里没强制加AND tenant_id = #{tenantId},就等于给注入留了后门 - 验证是否真生效:抓包看实际执行的 SQL 是否含完整 Schema 前缀,不含就是漏了
RLS 不是替代方案,而是最后一道补丁
即使你已做 Schema 切换,也必须启用 RLS 强制兜底。因为 DBA 直连、ETL 工具、报表脚本都可能绕过应用层逻辑。
PostgreSQL 示例:
CREATE POLICY tenant_schema_isolation ON users
USING (current_schema() = current_setting('app.current_tenant_schema')::name);
但注意:
- 必须先
ALTER TABLE users ENABLE ROW LEVEL SECURITY; - 应用层每次连接后需执行
SET LOCAL app.current_tenant_schema = 'tenant_a';,且该值必须来自 JWT 或 ThreadLocal,不可来自请求头拼接 - RLS 对
search_path无感知,它只校验当前会话的current_schema(),所以仍需确保SET操作本身不被注入
真正卡住风险的地方,从来不是“有没有建 Schema”,而是每次 SQL 执行前,是否同时满足:Schema 已绑定、SQL 含前缀、RLS 策略已启用、连接权限已锁定。少一个,隔离就断一环。











