39.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

需要注意的是,虽然视图可以用前文展示的技术来隐藏特定列的内容,它们不能可靠地在不可见行上隐藏数据,除非标志被设置。 例如,下面的视图是不安全的:

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,因为规划器会选择在执行NOT LIKE之前先执行tricky,因为前者的开销大。 即使禁止用户自定义一个新函数,内置函数也可以用在类似的攻击中(例如,大部分造型函数会在它们产生的错误信息中包含它们的输入值)。

类似的考虑应用于更新规则。在前一节的例子中,例子数据库中表的所有者可以把shoelace视图上的SELECTINSERTUPDATEDELETE权限授予其他人,但对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%';

Views created with the 使用security_barrier创建的视图的性能会远差于没有使用该选项的视图。通常,没有办法来避免这种现状:如果最快的候选计划可能在安全性上折衷,它就必须被拒绝。出于该原因,这个选在在默认情况下是没有启用的。

当处理没有副作用的函数时,查询规划器有更多的灵活性。这类函数被称为LEAKPROOF,并且包括很多简单常用的操作符,例如很多等于操作符。查询规划器可以安全地允许这类函数在查询执行过程中的任何点被计算,因为在用户不可见的行上调用它们将不会泄露关于不可见行的任何信息。更进一步,不接收参数或者不从安全屏障视图得到任何参数的函数不必被标记为LEAKPROOF以便被下推,因为它们从来不会从该视图接收数据。相反,一个可能会基于接收到的参数值抛出错误的函数(例如在溢出或被零除事件中抛出错误的函数)不是防泄漏的,并且如果它被应用在安全性视图的行过滤器之前,它可能会提供有关不可见行的有效信息。

有一点很重要的是理解:即使一个视图使用security_barrier选项创建,它也只在不可见元组不会被传递给可能不安全的函数的前提下才是安全的。用户可能也有其他方式来推断不可见数据;例如,他们可以使用EXPLAIN看到查询计划,或者针对视图来测量查询的运行时间。一个恶意攻击者可能有能力推断有关不可见数据的总量,或者甚至得到有关数据分布的某些信息或最常用值(因为这些东西可以影响计划的运行时间;或者甚至计划的选择,因为它们也被反映在优化器的统计数据中)。如果这类“隐通道”攻击很重要,那么授予任何到该数据的访问都可能是不明智的。