防止 updated_at 和 created_at 在 postgresql 中被黑客攻击

Prevent updated_at and created_at from being hacked in postgresql

假设我有:
帖子

id created_at updated_at ...

created_at的默认值是now(),我有updated_at[=43的触发器=] 像这样:

create trigger posts_updated_at before update on posts 
for each row execute procedure moddatetime (updated_at);

如何防止所有人手动更新日期?

  • created_at
  • updated_at

我想有一种标准方法可以保护这些字段(可能是列级安全性)?

像这样...

CREATE POLICY no_date_change ON posts
    FOR UPDATE
    USING (created_at === created_at);

created_at does not changecreated_at before = created_at after

保护这些字段的标准方法是什么?

J

实现这一点的最佳方法是同时使用 column privilege and security group

GRANT ... 更新(<column_name>..)在 到 ....

post 很长,因为我试图用步骤和示例来演示它。否则就是简单的几个步骤。

演示场景

Experiment table: pgvpd
Experiment schema: demo_schema
Admin User(full privilege on expriment table): postgres
General user (restricted access on necessary column): demo_user.

Table

select current_user;
create table demo_schema.pgvpd(txt varchar(10),createat date,updateat date);
insert into demo_schema.pgvpd values('one',current_date,current_date);
select * from demo_schema.pgvpd;

输出:

postgres=# select current_user;
 current_user
--------------
 postgres
(1 row)
postgres=# create table demo_schema.pgvpd(txt varchar(10),createat date,updateat date);
CREATE TABLE
postgres=# insert into demo_schema.pgvpd values('one',current_date,current_date);
INSERT 0 1
postgres=# select * from demo_schema.pgvpd;
 txt |  createat  |  updateat
-----+------------+------------
 one | 2022-02-06 | 2022-02-06
(1 row)

普通用户

create user demo_user password 'demo_user' valid until 'infinity';

输出:

postgres=# create user demo_user password 'demo_user' valid until 'infinity';
CREATE ROLE

撤销 table 的公开发布并验证

revoke all on demo_schema.pgvpd from public;
set session authorization demo_user;
select current_user;
select * from demo_schema.pgvpd; 

输出:

postgres=# revoke all on demo_schema.pgvpd from public;
REVOKE
postgres=# set session authorization demo_user;
SET
postgres=> select current_user;
 current_user
--------------
 demo_user
(1 row)
postgres=> select * from demo_schema.pgvpd;
ERROR:  permission denied for schema demo_schema
LINE 1: select * from demo_schema.pgvpd;

创建两个安全组,一个用于管理员(所有权限),一个用于所需的限制。 注意:最重要的部分 ...update(txt ) -> 只允许更新 'txt' 列,而不是日期列。

create group general_users;
grant usage on schema demo_schema to group general_users;
grant select,delete,insert,update(txt) on demo_schema.pgvpd to group general_users;
alter group general_users add user demo_user;

create group admin_users;
grant usage on schema demo_schema to group admin_users;
grant all on demo_schema.pgvpd to admin_users;
alter group admin_users add user postgres;

输出:

postgres=# create group general_users;
CREATE ROLE
postgres=# grant usage on schema demo_schema to group general_users;
GRANT
postgres=# grant select,delete,insert,update(txt) on demo_schema.pgvpd to group general_users;
GRANT
postgres=# alter group general_users add user demo_user;
ALTER ROLE

postgres=# create group admin_users;
CREATE ROLE
postgres=# grant usage on schema demo_schema to group admin_users;
GRANT
postgres=# grant all on demo_schema.pgvpd to admin_users;
GRANT
postgres=# alter group admin_users add user postgres;
ALTER ROLE

验证需要保护的列上的更新不允许一般用户使用,但允许管理员用户使用。

普通用户:

set session authorization demo_user;
select current_user;
select * from demo_schema.pgvpd;
insert into demo_schema.pgvpd values('two',current_date,current_date);
select * from demo_schema.pgvpd;
update  demo_schema.pgvpd set txt = 'two again' where txt='two';
select * from demo_schema.pgvpd;
update  demo_schema.pgvpd set createat = current_date where txt='two';

输出:

postgres=# set session authorization demo_user;
SET
postgres=> select current_user;
 current_user
--------------
 demo_user
(1 row)

postgres=> select * from demo_schema.pgvpd;
 txt |  createat  |  updateat
-----+------------+------------
 one | 2022-02-06 | 2022-02-06
(1 row)

postgres=> insert into demo_schema.pgvpd values('two',current_date,current_date);
INSERT 0 1
postgres=> select * from demo_schema.pgvpd;
 txt |  createat  |  updateat
-----+------------+------------
 one | 2022-02-06 | 2022-02-06
 two | 2022-02-06 | 2022-02-06
(2 rows)

postgres=> update  demo_schema.pgvpd set txt = 'two again' where txt='two';
UPDATE 1
postgres=> select * from demo_schema.pgvpd;
    txt    |  createat  |  updateat
-----------+------------+------------
 one       | 2022-02-06 | 2022-02-06
 two again | 2022-02-06 | 2022-02-06
(2 rows)

postgres=> update  demo_schema.pgvpd set createat = current_date where txt='two';
ERROR:  permission denied for table pgvpd

对于管理员用户:

postgres=> set session authorization postgres;
SET
postgres=# select current_user;
 current_user
--------------
 postgres
(1 row)
postgres=# update  demo_schema.pgvpd set createat = current_date where txt='two again';
UPDATE 1

建议:在生产 tables 中保持触发器的数量尽可能少,并针对性能进行完美调整。对于在其上执行的每个 DML,触发器是生产 table 的开销。

编辑:2022 年 2 月 7 日通过 行级安全策略.

进行演示

步骤 1:使用测试数据创建并填充 table。

create table posts
(
 id serial primary key,
 created_at timestamp,
 updated_at timestamp
);
grant all on posts to public;
grant all on posts_id_seq to public;

insert into posts(created_at,updated_at) values(current_timestamp,current_timestamp);
select pg_catalog.pg_sleep(10);
insert into posts(created_at,updated_at) values(current_timestamp,current_timestamp);

步骤 2:启用行级安全策略。

alter table posts enable row level security;

步骤 3:为 SELECT、INSERT、DELETE 创建一个许可策略。

create policy posts_insert on posts for insert with check(true);
create policy posts_delete on posts for delete using(true);
create policy posts_select on posts for select using(true);

第 4 步:创建函数 returns TRUE 或 FALSE 用于更新的许可策略。

create or replace function posts_update_check(id_new int,updated_at_new timestamp, created_at_new timestamp)
returns boolean
language plpgsql
as
$$
declare 
    row_variable posts%rowtype;
    flag boolean;
begin
    flag := true;
    select * from posts into row_variable where id=id_new;
    if (row_variable.updated_at != updated_at_new or row_variable.created_at != created_at_new) 
        then flag := false;
    end if;
    return flag; 
end; $$;

第 5 步:为 UPDATE 创建最终的所有重要权限策略。

create policy posts_update on posts for update using(true) with check(posts_update_check(id,updated_at,created_at));

步骤 6:运行 测试为 table.

的 non-owner
set session authorization demo_user;
select current_user;
select * from posts;
insert into posts(created_at,updated_at) values(current_timestamp,current_timestamp);
select pg_catalog.pg_sleep(5);
insert into posts(created_at,updated_at) values(current_timestamp,current_timestamp);
select * from posts;
delete from posts where id= 1;
update posts set id=1 where id=2;
update posts set created_at = current_timestamp where id=1;
update posts set updated_at = current_timestamp where id=1;

足迹:

postgres=# create table posts
postgres-# (
postgres(#  id serial primary key,
postgres(#  created_at timestamp,
postgres(#  updated_at timestamp
postgres(# );
CREATE TABLE
postgres=# grant all on posts to public;
GRANT
postgres=# grant all on posts_id_seq to public;
GRANT
postgres=#
postgres=#
postgres=# insert into posts(created_at,updated_at) values(current_timestamp,current_timestamp);
INSERT 0 1
postgres=# select pg_catalog.pg_sleep(10);
 pg_sleep
----------

(1 row)

postgres=# insert into posts(created_at,updated_at) values(current_timestamp,current_timestamp);
INSERT 0 1
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=# alter table posts enable row level security;
ALTER TABLE
postgres=#
postgres=#
postgres=#
postgres=# create policy posts_insert on posts for insert with check(true);
CREATE POLICY
postgres=# create policy posts_delete on posts for delete using(true);
CREATE POLICY
postgres=# create policy posts_select on posts for select using(true);
CREATE POLICY
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=# create or replace function posts_update_check(id_new int,updated_at_new timestamp, created_at_new timestamp)
postgres-# returns boolean
postgres-# language plpgsql
postgres-# as
postgres-# $$
postgres$# declare
postgres$# row_variable posts%rowtype;
postgres$# flag boolean;
postgres$# begin
postgres$# flag := true;
postgres$# select * from posts into row_variable where id=id_new;
postgres$# if (row_variable.updated_at != updated_at_new or row_variable.created_at != created_at_new)
postgres$#

postgres$# then flag := false;
postgres$# end if;
postgres$# return flag;
postgres$# end; $$;
CREATE FUNCTION
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=# create policy posts_update on posts for update using(true) with check(posts_update_check(id,updated_at,created_at));
CREATE POLICY
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=#
postgres=# set session authorization demo_user;
SET
postgres=> select current_user;
 current_user
--------------
 demo_user
(1 row)

postgres=> select * from posts;
 id |         created_at         |         updated_at
----+----------------------------+----------------------------
  1 | 2022-02-08 08:32:41.527004 | 2022-02-08 08:32:41.527004
  2 | 2022-02-08 08:32:51.536821 | 2022-02-08 08:32:51.536821
(2 rows)

postgres=> insert into posts(created_at,updated_at) values(current_timestamp,current_timestamp);
INSERT 0 1
postgres=> select pg_catalog.pg_sleep(5);
 pg_sleep
----------

(1 row)

postgres=> insert into posts(created_at,updated_at) values(current_timestamp,current_timestamp);
INSERT 0 1
postgres=> select * from posts;
 id |         created_at         |         updated_at
----+----------------------------+----------------------------
  1 | 2022-02-08 08:32:41.527004 | 2022-02-08 08:32:41.527004
  2 | 2022-02-08 08:32:51.536821 | 2022-02-08 08:32:51.536821
  3 | 2022-02-08 08:33:41.400015 | 2022-02-08 08:33:41.400015
  4 | 2022-02-08 08:33:46.411654 | 2022-02-08 08:33:46.411654
(4 rows)

postgres=> delete from posts where id= 1;
DELETE 1
postgres=> update posts set id=1 where id=2;
UPDATE 1
postgres=> update posts set created_at = current_timestamp where id=1;
ERROR:  new row violates row-level security policy for table "posts"
postgres=> update posts set updated_at = current_timestamp where id=1;
ERROR:  new row violates row-level security policy for table "posts"
postgres=>


编辑 2:2022 年 2 月 7 日,演示触发器的影响。

设置: 相同 tables

帖子 - 有安全规则

Posts_1 - 有触发器

两个新创建的 table 具有相同的行。

posts:

postgres=> \d posts
                                        Table "public.posts"
   Column   |            Type             | Collation | Nullable |              Default
------------+-----------------------------+-----------+----------+-----------------------------------
 id         | integer                     |           | not null | nextval('posts_id_seq'::regclass)
 created_at | timestamp without time zone |           |          |
 updated_at | timestamp without time zone |           |          |
Indexes:
    "posts_pkey" PRIMARY KEY, btree (id)
Policies (forced row security enabled):
    POLICY "posts_delete" FOR DELETE
      USING (true)
    POLICY "posts_insert" FOR INSERT
      WITH CHECK (true)
    POLICY "posts_select" FOR SELECT
      USING (true)
    POLICY "posts_update" FOR UPDATE
      USING (true)
      WITH CHECK (posts_update_check(id, updated_at, created_at))
  postgres=> select * from posts;
 id |         created_at         |         updated_at
----+----------------------------+----------------------------
  8 | 2022-02-08 16:23:20.48457  | 2022-02-08 16:23:20.48457
  9 | 2022-02-08 16:23:30.501669 | 2022-02-08 16:23:30.501669
(2 rows)

posts_1:

postgres=> \d posts_1
                                        Table "public.posts_1"
   Column   |            Type             | Collation | Nullable |               Default
------------+-----------------------------+-----------+----------+-------------------------------------
 id         | integer                     |           | not null | nextval('posts_1_id_seq'::regclass)
 created_at | timestamp without time zone |           |          |
 updated_at | timestamp without time zone |           |          |
Indexes:
    "posts_1_pkey" PRIMARY KEY, btree (id)
Triggers:
    on_posts_1_update BEFORE UPDATE ON posts_1 FOR EACH ROW EXECUTE FUNCTION posts_1_created_at()

postgres=> select * from posts_1;
 id |         created_at         |         updated_at
----+----------------------------+----------------------------
  3 | 2022-02-08 16:17:11.792555 | 2022-02-08 16:17:11.792555
  4 | 2022-02-08 16:17:21.800956 | 2022-02-08 16:17:21.800956
(2 rows)

postsposts_1

的性能比较

posts(基于策略)

postgres=> explain (analyze,timing,buffers)   update posts_1 set id=80 where id=8;
                                               QUERY PLAN
--------------------------------------------------------------------------------------------------------
 Update on posts_1  (cost=0.00..1.02 rows=1 width=26) (actual time=0.017..0.020 rows=0 loops=1)
   Buffers: shared hit=1
   ->  Seq Scan on posts_1  (cost=0.00..1.02 rows=1 width=26) (actual time=0.014..0.015 rows=0 loops=1)
         Filter: (id = 8)
         Rows Removed by Filter: 2
         Buffers: shared hit=1
 Planning Time: 0.076 ms
 Execution Time: 0.054 ms
(8 rows)

posts_1(基于触发器)

postgres=> explain (analyze,timing,buffers)   update posts_1 set id=40 where id=4;
                                               QUERY PLAN
--------------------------------------------------------------------------------------------------------
 Update on posts_1  (cost=0.00..1.02 rows=1 width=26) (actual time=0.112..0.115 rows=0 loops=1)
   Buffers: shared hit=3 read=1 dirtied=2
   ->  Seq Scan on posts_1  (cost=0.00..1.02 rows=1 width=26) (actual time=0.015..0.017 rows=1 loops=1)
         Filter: (id = 4)
         Rows Removed by Filter: 1
         Buffers: shared hit=1
 Planning Time: 0.075 ms
 Trigger on_posts_1_update: time=0.033 calls=1
 Execution Time: 0.150 ms
(9 rows)

执行计划的区别在于触发器的额外行。

Trigger on_posts_1_update: time=0.033 calls=1

我相信即使是政策也必须执行代码,但看起来触发器的成本更高。 但是,如果通过触发器引入的更新量和延迟是acceptable,则可以忽略trade-off。你真的可以通过基准测试来辨别它。

I have a trigger for updated_at

所以你有一个设置 updated_at 的触发器。因此,任何人都无法更新此字段,因为触发器会覆盖它。

那么您只需要在触发器中进行额外检查 - 类似于 if OLD.created_at<>NEW.created_at then raise ERROR.

我还会 运行 在 CREATE 上使用类似的触发器,以在创建行时也强制 created_at 和 updated_at。