租户上下文与SQL结构必须强绑定,tenant_id须硬编码进所有业务SQL或由框架/中间件强制注入,严禁开发自觉添加;漏写条件、误用CURRENT_USER、未校验动态Schema名、绕过视图直查基表等均会导致越权与注入风险叠加。

多租户系统里,SQL注入和越权不是两个独立问题,而是一体两面:注入一旦成功,往往直接绕过租户隔离逻辑,查到其他租户数据。核心防线不在“防注入”或“防越权”单点,而在**租户上下文与SQL结构的强绑定**。
tenant_id 必须作为查询条件硬编码进所有业务SQL
行级隔离方案下,tenant_id 不能靠应用层“自觉加”,必须由框架或中间件强制注入。常见错误是开发在DAO里写 SELECT * FROM orders WHERE id = ?,漏掉 tenant_id = ? 条件。
- MyBatis 用拦截器,在
Executor.update和query前自动追加AND tenant_id = #{tenantId},且对INSERT语句自动补全tenant_id字段值 - ShardingSphere 配置
tenant_id为分片键,所有路由、改写、归并逻辑都基于它,但注意:它不拦截SELECT * FROM orders这类无条件查询——得配合sqlHint或自定义SQLRewriteRule - PostgreSQL 的 RLS(行级安全策略)要求每个用户连接对应租户角色,策略中写
USING (tenant_id = current_setting('app.tenant_id')),但前提是应用每次连接前执行SET app.tenant_id = '123',且该 setting 不被用户输入污染
动态表名/Schema 名必须走白名单映射
Schema 级隔离时,SET search_path TO tenant_abc 的 tenant_abc 若来自用户输入(如 URL path 中的 /api/tenant_abc/orders),就等于把租户名当 SQL 结构暴露出去——参数化查询对它完全无效。
- 维护一个只读配置数组:
$valid_tenants = ['tenant_abc' => 'tenant_abc', 'tenant_xyz' => 'tenant_xyz'],用in_array($input, $valid_tenants, true)校验,不接受任何正则过滤或转义 - PostgreSQL 中禁止在视图里用
CURRENT_USER动态过滤租户数据(如CREATE VIEW my_data AS SELECT * FROM orders WHERE tenant_id = CURRENT_USER),因为CURRENT_USER是数据库角色名,不是业务租户ID,且无法与 JWT 中的tenant_id对齐 - 连接池层(如 PgBouncer)不支持 per-connection 的
search_path设置,所以必须在应用获取连接后立刻执行SET search_path,不能依赖连接初始化脚本
敏感字段脱敏必须固化在数据库视图里
应用层做手机号掩码(如 substr(phone, 0, 3) . '****' . substr(phone, -4))不可靠:任意一个开发漏写,或 ORM 自动生成的 SELECT * 就会把原始字段吐出去。
- 建
user_safe_view视图,里面用REGEXP_REPLACE(phone, '(\d{3})\d{4}(\d{4})', '\1****\2')(PostgreSQL)或CONCAT(LEFT(phone,3), '****', RIGHT(phone,4))(MySQL 5.7)直接输出脱敏结果 - 应用账号只授予该视图的
SELECT权限,不给底层users表权限;即使攻击者通过注入拿到视图名,也只能看到已脱敏字段 - 视图里不能带
WHERE tenant_id = ?这种参数化条件——视图定义是静态的,运行时变量不生效;租户隔离仍需靠上层 RLS 或连接级search_path
数据库账号权限必须按租户粒度隔离
哪怕用了 RLS 或 Schema 隔离,如果应用统一用一个高权限账号连接数据库,攻击者注入成功后就能 UNION SELECT 其他租户表,或直接 SELECT * FROM pg_tables 枚举结构。
- 为每个租户创建独立数据库用户(如
tenant_123_app),只授予其对应 Schema 的USAGE和SELECT/INSERT/UPDATE权限,禁用CREATE、DROP、pg_read_all_data - Schema 级隔离时,禁止跨 Schema 查询:
SELECT a.*, b.name FROM tenant_a.users a JOIN tenant_b.orders b ON ...这类语句必须显式拒绝,可在 PostgreSQL 中用row_security+SECURITY DEFINER函数封装,或在 API 网关层校验 SQL AST - DBA 手工运维时,绝不用租户账号执行
psql -U tenant_123_app,而应切换到专用运维账号;否则一次\dt就可能暴露租户 Schema 列表
最易被忽略的点是:RLS 策略开启后,SELECT 语句受控,但 INSERT 和 UPDATE 仍可能被绕过——比如插入时没校验 tenant_id 是否匹配当前会话上下文,导致数据写错 Schema 或行。这需要在触发器或应用层双重检查,不能只信 RLS。

















