
在 PostgreSQL 中,SECURITY DEFINER 函数允许以函数定义者的权限执行逻辑,而非调用者权限。这在封装敏感操作(如日志写入、配置更新)时很有用,但若设计不当,极易成为权限提升的入口。
为什么 SECURITY DEFINER 函数容易引发提权?
核心在于“信任错位”:函数体内的 SQL 语句(尤其是动态 SQL)若拼接了用户可控输入,就可能绕过调用者原本受限的权限边界。例如,一个以超级用户定义、但接受表名参数的函数,若未校验表名合法性,调用者即可通过传入 'pg_authid; DROP TABLE users--' 类似字符串触发恶意操作。
常见高危模式包括:
- 使用
EXECUTE ... USING但未对变量做白名单校验或转义 - 函数内直接拼接用户输入到 SQL 字符串(如
'SELECT * FROM ' || user_table) - 依赖
CURRENT_USER或SESSION_USER做访问控制,却忽略其在 SECURITY DEFINER 下恒为定义者 - 函数调用其他未加防护的 SECURITY DEFINER 函数,形成权限传递链
安全编写 SECURITY DEFINER 函数的关键原则
不依赖“输入看起来安全”,而应强制执行最小权限与输入净化:
-
禁用动态拼接:优先使用静态 SQL;必须动态时,仅允许从预定义枚举或系统目录(如
pg_tables)中查出的合法标识符,并用quote_ident()包裹 -
显式限定对象权限:函数内所有涉及的表、序列、函数等,定义者账户必须仅拥有实际需要的权限(如仅
SELECT,不给UPDATE),避免“定义者权限过大”放大风险 -
剥离敏感上下文:不在函数内调用
current_setting('app.user_id')等依赖会话变量的逻辑,改用显式参数传入并验证 -
记录与审计:对关键 SECURITY DEFINER 函数的调用,写入专用审计表(注意审计表自身也需权限隔离),包含
now()、current_user、session_user、参数摘要
运行时防护与监控建议
单靠编码规范不够,需结合数据库层管控:
- 定期扫描:
SELECT proname, proowner::regrole, prosecdef FROM pg_proc WHERE prosecdef,识别所有 SECURITY DEFINER 函数,逐个评估必要性与实现方式 - 限制定义者身份:避免使用
postgres或其他高权限角色定义业务函数;为每类敏感操作创建专用低权限角色(如log_writer),仅授必要对象权限 - 启用
log_statement = 'mod'或更细粒度的pg_audit扩展,捕获 SECURITY DEFINER 函数内部执行的 DDL/DML - 测试用例覆盖:针对每个函数,构造含 SQL 注入片段、跨 schema 表名、保留字等的异常输入,验证是否报错而非静默执行
替代方案:多数场景其实不需要 SECURITY DEFINER
与其承担提权风险,不如优先考虑更安全的替代路径:
- 行级安全策略(RLS):对共享表统一管控,让普通用户只能看到/改写自己数据,无需特权函数介入
- VIEW + GRANT:用视图封装查询逻辑,配合列权限控制,调用者仍以自身权限运行
- 应用程序层代理:将敏感操作收口到应用服务,由后端统一鉴权并以专用数据库连接执行,数据库侧保持零特权函数
-
SECURITY INVOKER + 授权链:让函数以调用者权限运行,再通过
GRANT EXECUTE ON FUNCTION和对象权限组合实现精细控制










