Appearance
PostgreSQL 函数安全性深度解析
概述
PostgreSQL 的函数、触发器和行级安全策略(RLS)是强大的功能,允许用户在数据库服务器端执行自定义代码。然而,这种强大的功能也带来了重大的安全风险,需要深入理解和谨慎管理。
安全警告
函数在数据库后端服务器进程内部运行,具有数据库服务器守护进程的操作系统权限,因此安全配置至关重要。
安全风险分析
1. 代码注入与"木马"攻击
函数、触发器和行级安全策略允许用户将代码插入到后端服务器中,这为恶意用户提供了"木马"攻击的机会。
实际攻击场景示例
问题陈述:恶意用户创建一个看似无害的函数,实际上会记录敏感信息。
恶意函数示例:
sql
-- 恶意用户创建的"无害"函数
CREATE OR REPLACE FUNCTION public.get_user_count()
RETURNS INTEGER AS $$
DECLARE
user_count INTEGER;
sensitive_data TEXT;
BEGIN
-- 表面功能:返回用户数量
SELECT COUNT(*) INTO user_count FROM users;
-- 隐藏的恶意行为:记录敏感信息到日志表
SELECT string_agg(username || ':' || password_hash, ',')
INTO sensitive_data
FROM users
WHERE is_admin = true;
-- 将敏感数据写入隐藏表
INSERT INTO malicious_log (data, timestamp)
VALUES (sensitive_data, NOW());
RETURN user_count;
END;
$$ LANGUAGE plpgsql;
-- 其他用户无意中调用这个函数
SELECT get_user_count(); -- 看起来很正常的查询
分析过程:
- 函数表面上只是返回用户数量,看起来无害
- 但内部却在收集管理员用户的敏感信息
- 每次被调用时都会泄露数据到隐藏的日志表
- 其他用户很难发现这种隐藏行为
2. 系统权限滥用
函数在数据库服务器进程内运行,具有服务器守护进程的操作系统权限,可能被用来:
- 访问服务器文件系统
- 执行系统命令
- 修改服务器配置
- 绕过数据库访问控制
危险操作示例
sql
-- 使用 C 语言编写的不可信函数示例
CREATE OR REPLACE FUNCTION read_server_file(filename TEXT)
RETURNS TEXT AS $$
#include "postgres.h"
#include "fmgr.h"
#include <stdio.h>
#include <stdlib.h>
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(read_server_file);
Datum read_server_file(PG_FUNCTION_ARGS) {
char *filename = text_to_cstring(PG_GETARG_TEXT_PP(0));
FILE *file;
char *content;
long file_size;
// 直接读取服务器文件系统中的文件
file = fopen(filename, "r");
if (!file) {
PG_RETURN_NULL();
}
// 读取文件内容
fseek(file, 0, SEEK_END);
file_size = ftell(file);
rewind(file);
content = palloc(file_size + 1);
fread(content, 1, file_size, file);
content[file_size] = '\0';
fclose(file);
PG_RETURN_TEXT_P(cstring_to_text(content));
}
$$ LANGUAGE c;
-- 恶意使用:读取系统敏感文件
SELECT read_server_file('/etc/passwd'); -- 读取系统用户信息
SELECT read_server_file('/etc/postgresql/postgresql.conf'); -- 读取数据库配置
DANGER
上述示例展示了不可信语言函数的危险性,它们可以直接访问操作系统资源,绕过所有数据库安全控制。
核心保护策略
1. 严格的权限控制
最强保护措施:严格控制谁可以定义对象(函数、触发器等)。
权限管理最佳实践
sql
-- 创建专门的函数开发角色
CREATE ROLE function_developer;
-- 只给特定用户函数创建权限
GRANT function_developer TO trusted_developer;
-- 撤销公共模式的创建权限
REVOKE CREATE ON SCHEMA public FROM PUBLIC;
-- 创建专门的函数模式
CREATE SCHEMA trusted_functions;
GRANT CREATE ON SCHEMA trusted_functions TO function_developer;
-- 监控函数创建的触发器
CREATE OR REPLACE FUNCTION audit_function_creation()
RETURNS event_trigger AS $$
BEGIN
INSERT INTO function_audit_log (
function_name,
creator,
creation_time,
function_definition
) VALUES (
tg_object_identity,
current_user,
NOW(),
pg_get_functiondef(tg_objectid)
);
-- 发送通知给管理员
PERFORM pg_notify('function_created',
'User ' || current_user || ' created function ' || tg_object_identity);
END;
$$ LANGUAGE plpgsql;
-- 创建事件触发器
CREATE EVENT TRIGGER function_creation_audit
ON ddl_command_end
WHEN TAG IN ('CREATE FUNCTION', 'CREATE OR REPLACE FUNCTION')
EXECUTE FUNCTION audit_function_creation();
2. 安全的 search_path 配置
关键策略:从 search_path 中删除任何允许不可信用户创建对象的模式。
问题场景分析
sql
-- 危险的默认 search_path
SHOW search_path;
-- 结果: "$user", public
-- 攻击者可以在 public 模式中创建同名函数来劫持调用
CREATE SCHEMA attacker_schema;
GRANT CREATE ON SCHEMA attacker_schema TO attacker_user;
-- 攻击者创建恶意函数
SET ROLE attacker_user;
CREATE FUNCTION public.calculate_bonus(salary NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
-- 记录敏感的薪资信息
INSERT INTO data_theft_log VALUES (salary, current_user, NOW());
-- 返回一个看似正常的结果
RETURN salary * 0.1;
END;
$$ LANGUAGE plpgsql;
安全配置解决方案
sql
-- 为每个数据库角色设置安全的 search_path
ALTER ROLE application_user SET search_path = trusted_schema, pg_catalog;
-- 在函数中显式指定模式
CREATE OR REPLACE FUNCTION trusted_schema.calculate_bonus(salary NUMERIC)
RETURNS NUMERIC AS $$
BEGIN
-- 使用完全限定的表名
INSERT INTO trusted_schema.bonus_calculations (
user_id,
salary,
bonus_amount,
calculation_time
) VALUES (
current_setting('app.user_id')::INTEGER,
salary,
salary * 0.1,
NOW()
);
RETURN salary * 0.1;
END;
$$ LANGUAGE plpgsql
SECURITY DEFINER -- 以函数所有者权限运行
SET search_path = trusted_schema, pg_catalog; -- 函数内部使用安全的 search_path
3. 引用可信所有者的对象
策略:编写仅引用具有可信所有者的对象的查询。
sql
-- 查看对象所有者
SELECT
n.nspname AS schema_name,
p.proname AS function_name,
pg_get_userbyid(p.proowner) AS owner,
CASE
WHEN pg_get_userbyid(p.proowner) IN ('postgres', 'trusted_admin')
THEN 'TRUSTED'
ELSE 'UNTRUSTED'
END AS trust_status
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname NOT IN ('information_schema', 'pg_catalog')
ORDER BY trust_status, schema_name, function_name;
-- 创建视图只显示可信函数
CREATE VIEW trusted_functions AS
SELECT
n.nspname AS schema_name,
p.proname AS function_name,
pg_get_functiondef(p.oid) AS definition
FROM pg_proc p
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE pg_get_userbyid(p.proowner) IN ('postgres', 'trusted_admin')
AND n.nspname NOT IN ('information_schema', 'pg_catalog');
可信与不可信语言
PostgreSQL 将函数语言分为两类:可信和不可信。
可信语言特性
可信语言(如 PL/pgSQL、PL/Python、PL/Perl)提供安全沙箱环境:
sql
-- PL/pgSQL 示例 - 可信语言
CREATE OR REPLACE FUNCTION safe_user_lookup(user_id INTEGER)
RETURNS TABLE(username TEXT, email TEXT) AS $$
BEGIN
-- 只能访问数据库内的对象
-- 无法访问文件系统或执行系统命令
RETURN QUERY
SELECT u.username, u.email
FROM users u
WHERE u.id = user_id
AND u.is_active = true;
END;
$$ LANGUAGE plpgsql;
不可信语言风险
不可信语言(如 C、C++)可以:
- 直接访问内存
- 调用系统函数
- 访问文件系统
- 执行任意操作系统命令
sql
-- 检查当前用户是否为超级用户
SELECT
current_user,
usesuper
FROM pg_user
WHERE usename = current_user;
-- 只有超级用户才能创建不可信语言函数
CREATE LANGUAGE c; -- 需要超级用户权限
不可信语言安全管理
sql
-- 创建受限的函数创建角色
CREATE ROLE secure_function_creator;
-- 即使是超级用户,也应该限制不可信语言的使用
CREATE OR REPLACE FUNCTION create_c_function_with_approval(
function_name TEXT,
function_code TEXT,
approver_signature TEXT
)
RETURNS BOOLEAN AS $$
DECLARE
approval_valid BOOLEAN;
BEGIN
-- 验证审批签名
SELECT verify_digital_signature(function_code, approver_signature)
INTO approval_valid;
IF NOT approval_valid THEN
RAISE EXCEPTION 'Invalid approval signature for C function creation';
END IF;
-- 记录审计日志
INSERT INTO c_function_audit (
function_name,
creator,
approver_signature,
creation_time,
function_code
) VALUES (
function_name,
current_user,
approver_signature,
NOW(),
function_code
);
-- 执行函数创建(需要额外的安全检查)
EXECUTE format('CREATE OR REPLACE FUNCTION %s %s LANGUAGE c',
function_name, function_code);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
实际应用与监控
安全监控系统
sql
-- 创建函数执行监控表
CREATE TABLE function_execution_log (
id SERIAL PRIMARY KEY,
function_name TEXT NOT NULL,
caller_user TEXT NOT NULL,
execution_time TIMESTAMP DEFAULT NOW(),
parameters JSONB,
execution_duration INTERVAL,
result_summary TEXT
);
-- 创建监控函数的包装器
CREATE OR REPLACE FUNCTION monitored_function_wrapper(
function_name TEXT,
parameters JSONB DEFAULT '{}'::JSONB
)
RETURNS JSONB AS $$
DECLARE
start_time TIMESTAMP;
end_time TIMESTAMP;
result JSONB;
execution_duration INTERVAL;
BEGIN
start_time := clock_timestamp();
-- 记录函数调用
INSERT INTO function_execution_log (
function_name,
caller_user,
parameters
) VALUES (
function_name,
current_user,
parameters
);
-- 这里应该实际执行目标函数
-- 为了示例,我们模拟一个结果
result := jsonb_build_object(
'status', 'success',
'message', 'Function executed safely'
);
end_time := clock_timestamp();
execution_duration := end_time - start_time;
-- 更新执行日志
UPDATE function_execution_log
SET
execution_duration = execution_duration,
result_summary = result::TEXT
WHERE function_name = monitored_function_wrapper.function_name
AND caller_user = current_user
AND execution_time = (
SELECT MAX(execution_time)
FROM function_execution_log
WHERE function_name = monitored_function_wrapper.function_name
AND caller_user = current_user
);
RETURN result;
END;
$$ LANGUAGE plpgsql;
安全审计报告
sql
-- 生成函数安全审计报告
CREATE OR REPLACE FUNCTION generate_function_security_report()
RETURNS TABLE(
category TEXT,
function_name TEXT,
owner TEXT,
risk_level TEXT,
recommendations TEXT
) AS $$
BEGIN
RETURN QUERY
WITH function_analysis AS (
SELECT
n.nspname AS schema_name,
p.proname AS func_name,
pg_get_userbyid(p.proowner) AS func_owner,
l.lanname AS language,
CASE
WHEN l.lanpltrusted THEN 'TRUSTED'
ELSE 'UNTRUSTED'
END AS trust_status,
CASE
WHEN pg_get_userbyid(p.proowner) = 'postgres' THEN 'LOW'
WHEN pg_get_userbyid(p.proowner) IN (
SELECT rolname FROM pg_roles WHERE rolsuper
) THEN 'MEDIUM'
ELSE 'HIGH'
END AS risk_level
FROM pg_proc p
JOIN pg_language l ON p.prolang = l.oid
JOIN pg_namespace n ON p.pronamespace = n.oid
WHERE n.nspname NOT IN ('information_schema', 'pg_catalog')
)
SELECT
'Function Security Analysis' AS category,
schema_name || '.' || func_name AS function_name,
func_owner AS owner,
risk_level,
CASE
WHEN risk_level = 'HIGH' AND trust_status = 'UNTRUSTED' THEN
'CRITICAL: Untrusted function by non-superuser - Review immediately'
WHEN risk_level = 'HIGH' THEN
'HIGH: Function by untrusted user - Audit code and permissions'
WHEN trust_status = 'UNTRUSTED' THEN
'MEDIUM: Untrusted language - Ensure proper approval process'
ELSE
'LOW: Function appears secure'
END AS recommendations
FROM function_analysis
ORDER BY
CASE risk_level
WHEN 'HIGH' THEN 1
WHEN 'MEDIUM' THEN 2
ELSE 3
END,
function_name;
END;
$$ LANGUAGE plpgsql;
-- 执行安全审计
SELECT * FROM generate_function_security_report();
最佳实践总结
核心安全原则
- 最小权限原则:只授予必要的函数创建和执行权限
- 代码审计:所有自定义函数都应经过安全审计
- 环境隔离:在受控环境中测试函数
- 持续监控:实施函数执行监控和异常检测
安全检查清单
检查项 | 状态 | 备注 |
---|---|---|
限制函数创建权限 | ✅ | 只有受信任用户可创建函数 |
配置安全的 search_path | ✅ | 排除不可信模式 |
审计所有自定义函数 | ⚠️ | 定期审查函数代码 |
监控函数执行 | ⚠️ | 实施执行日志记录 |
限制不可信语言使用 | ✅ | 只有超级用户可用 |
实施代码签名 | ❌ | 考虑对关键函数实施 |
IMPORTANT
函数安全性是数据库整体安全策略的重要组成部分。定期审查和更新安全措施是确保系统安全的关键。
通过理解这些安全风险和保护措施,数据库管理员可以在享受 PostgreSQL 强大功能的同时,确保系统的安全性和完整性。