41.5. 规则和权限 #
由于 PostgreSQL 规则系统会重写查询,因此可能访问原始查询中并未使用的其他表或视图。使用更新规则时,这甚至可能包括对表的写访问。
重写规则没有单独的所有者。关系(表或视图)的所有者会自动成为为其定义的重写规则的所有者。PostgreSQL 规则系统改变了默认访问控制系统的行为。由于规则而使用的关系都会根据规则所有者的权限进行检查,而不是调用规则的用户。这意味着用户只需要对其查询中明确命名的表/视图具有所需的权限。
例如,某个用户有一份电话号码列表,其中一部分是私人的,另一部分对办公室助理有用。该用户可以这样构造:
CREATE TABLE phone_data (person text, phone text, private boolean);
CREATE VIEW phone_number AS
SELECT person, CASE WHEN NOT private THEN phone END AS phone
FROM phone_data;
GRANT SELECT ON phone_number TO assistant;
除了该用户本人(以及数据库超级用户)之外,没有人可以访问 phone_data 表。但由于 GRANT 的存在,助理可以对 phone_number 视图执行 SELECT。规则系统会把对 phone_number 的 SELECT 重写成对 phone_data 的 SELECT。由于该用户是 phone_number 的所有者,因此也是规则的所有者,对 phone_data 的读访问会按照该用户的权限进行检查,于是查询被允许。同时,对 phone_number 本身的访问检查仍会执行,但这是针对调用用户进行的,因此除了用户本人和助理之外,没有其他人能使用它。
权限是逐条规则检查的。因此,目前助理是唯一能看到公开电话号码的人。但助理还可以再建立一个视图,并把该视图授予公众访问。这样,任何人都可以通过助理的视图看到 phone_number 中的数据。助理做不到的是创建一个直接访问 phone_data 的视图。(其实助理可以创建,但它不会起作用,因为每次访问都会在权限检查时被拒绝。)而且,一旦用户发现助理开放了其 phone_number 视图,用户就可以撤销助理的访问权限。那样一来,对助理视图的任何访问都会立即失败。
这种逐条规则的检查看起来似乎像是一个安全漏洞,但实际上并不是。因为即使不这样工作,助理也可以建立一个与 phone_number 具有相同列的表,每天把数据复制进去。那样一来,这些就是助理自己的数据,助理同样可以把访问权限授予任何人。GRANT 的含义本来就是“我信任你”。如果某个你信任的人做了上面的事,那么该重新考虑这份信任,并使用 REVOKE 了。
需要注意的是,虽然视图可以用前文展示的技术来隐藏某些列的内容,但除非设置了 security_barrier 标志,否则它们不能被用来可靠地隐藏不可见行中的数据。例如,下面这个视图就是不安全的:
CREATE VIEW phone_number AS
SELECT person, phone FROM phone_data WHERE phone NOT LIKE '412%';
这个视图看起来似乎很安全,因为规则系统会把对 phone_number 的任何 SELECT 重写为对 phone_data 的 SELECT,并附加条件,要求只处理 phone 不以 412 开头的项。但是,如果用户可以创建自己的函数,就不难让规划器在 NOT LIKE 表达式之前先执行用户定义函数。例如:
CREATE FUNCTION tricky(text, text) RETURNS bool AS $$
BEGIN
RAISE NOTICE '% => %', $1, $2;
RETURN true;
END;
$$ LANGUAGE plpgsql COST 0.0000000000000000000001;
SELECT * FROM phone_number WHERE tricky(person, phone);
phone_data 表中的每个人和电话号码都会被打印为一条 NOTICE,因为规划器会选择先执行廉价的 tricky 函数,再执行更昂贵的 NOT LIKE。即使禁止用户定义新函数,内置函数也可以用于类似攻击。(例如,大多数类型转换函数都会在其错误消息中包含输入值。)
类似的考虑也适用于更新规则。在上一节的示例中,示例数据库中那些表的所有者可以把 shoelace 视图上的 SELECT、INSERT、UPDATE 和 DELETE 权限授予其他用户,但对 shoelace_log 只授予 SELECT 权限。写日志记录的规则动作仍会成功执行,因此其他用户可以看到日志记录。但他们不能伪造记录,也不能操纵或删除已有记录。在这个例子里,不存在通过说服规划器改变操作顺序来破坏规则的可能性,因为唯一引用 shoelace_log 的规则是一条无条件的 INSERT。在更复杂的场景中,情况未必如此。
当视图需要提供行级安全时,应为该视图应用 security_barrier 属性。这可以防止在视图完成其工作之前,将行中的值传给恶意选择的函数和操作符。例如,如果上面展示的视图按以下方式创建,就会是安全的:
CREATE VIEW phone_number WITH (security_barrier) AS
SELECT person, phone FROM phone_data WHERE phone NOT LIKE '412%';
使用 security_barrier 创建的视图,其性能可能远低于未使用此选项的视图。通常无法避免这一点:如果最快的执行计划可能危及安全,就必须拒绝它。因此,此选项默认不启用。
在处理没有副作用的函数时,查询规划器有更大的灵活性。这类函数称为 LEAKPROOF,其中包括许多简单且常用的操作符,例如大量等值操作符。规划器可以安全地允许这类函数在查询执行过程中的任意位置求值,因为即便把它们应用到用户不可见的行上,也不会泄露这些不可见行的信息。此外,不带参数的函数,或者没有从安全屏障视图接收任何参数的函数,即使未被标记为 LEAKPROOF,也可以下推,因为它们根本不会接收到来自视图的数据。相反,那些可能根据参数值抛出错误的函数(例如在发生溢出或除零时抛错的函数)就不具备防泄漏性;如果在安全视图的行过滤之前对其求值,就可能泄露关于不可见行的重要信息。
需要理解的一点是,即使一个视图是用 security_barrier 选项创建的,它的安全性也只限于这样一种意义:不可见元组的内容不会被传递给可能不安全的函数。用户仍然可能通过其他方式推断不可见数据;例如,他们可以使用 EXPLAIN 查看查询计划,或者测量针对该视图执行查询所需的时间。恶意攻击者或许能够推断出不可见数据的大致数量,甚至获得有关数据分布或高频值的一些信息(因为这些因素可能影响计划运行时间,甚至影响计划的选择,因为它们同样反映在优化器统计信息中)。如果这类“隐通道”攻击值得担心,那么向任何人授予这类数据的访问权限本身就可能是不明智的。