Ai đã sửa dữ liệu khách? Nhật ký kiểm toán cho ứng dụng nội bộ

Nhật ký kiểm toán cho biết ai đã sửa dữ liệu khách, lúc nào, từ giá trị nào. Bài dựa trên tài liệu OWASP, PostgreSQL, pgAudit và Supabase, kèm mã SQL đã chạy thử.
Một nhân viên kinh doanh báo khách VIP của mình bị hạ về hạng thường. Bảng khách chỉ cho thấy giá trị hiện tại, không cho biết ai sửa, lúc nào, từ giá trị nào. Đây là ví dụ minh họa, không phải sự cố thật.
Bài này dựa trên tài liệu của OWASP, PostgreSQL, pgAudit và Supabase, đối chiếu đến ngày 04/11/2026. Click Star chạy thử mã SQL trên PGlite 0.5.15 (PostgreSQL 18.6 chạy trong WebAssembly) với dữ liệu giả.
Bài trả lời: nhật ký kiểm toán (audit log) cần ghi gì, ghi bằng cách nào và giữ sao cho không ai sửa được. Click Star đã có bài riêng về giám sát website và báo cáo sau sự cố.
Điểm chính
- Nhật ký kiểm toán trả lời "ai làm gì, trên dữ liệu nào, lúc nào". OWASP khuyên thường nên tách nó khỏi nhật ký bảo mật.
- Mỗi dòng phải trả lời khi nào, ở đâu, ai và việc gì (OWASP). Với dữ liệu khách, Click Star đề xuất thêm giá trị cũ và mới.
- Mật khẩu, khóa, token, số thẻ thường không nên ghi thẳng vào nhật ký. Số điện thoại và email có thể cần che hoặc bỏ, vì nhật ký là bản sao thứ hai của dữ liệu khách (nhận định của Click Star).
- Trigger bắt lệnh thêm, sửa, xóa từng dòng dù đến từ đâu, nếu còn bật và không ở chế độ replica;
TRUNCATEphải khai thêm, lượt đọc không bắt được. pgAudit ghi câu lệnh, kể cả lượt đọc, nhưng tài liệu không nêu việc lưu giá trị cũ. - Trigger không tự biết người dùng cuối: tên người thao tác do ứng dụng truyền vào nên giả mạo được.
- Bảng nhật ký phải chỉ thêm: thu quyền, chặn sửa và xóa bằng trigger, cảnh báo khi ngừng ghi, chép ra nơi khác.
- Thời hạn giữ và việc ghi dữ liệu cá nhân là câu hỏi pháp lý; Click Star chưa đọc luật Việt Nam về việc này.

Nhật ký kiểm toán khác nhật ký lỗi ở đâu
Nhật ký lỗi cho biết hệ thống hỏng ở đâu. Nhật ký kiểm toán cho biết ai đã thêm, sửa, xóa hay xuất dữ liệu nào.
Trang "Logging Cheat Sheet" của OWASP (không ghi ngày, đọc 04/11/2026) nói nhật ký kiểm toán thường thu cho mục đích khác nhật ký sự kiện bảo mật, nên thường cần tách riêng.
OWASP Top 10:2025, mục A09 (không ghi ngày, đọc 04/11/2026), khuyên mọi giao dịch có dấu vết kiểm toán kèm kiểm soát toàn vẹn, ví dụ bảng chỉ thêm.
Mỗi dòng nên ghi gì, và không nên ghi gì
Theo Logging Cheat Sheet, mật khẩu, token, mã phiên, chuỗi kết nối cơ sở dữ liệu, khóa mã hóa và dữ liệu thẻ thanh toán nên bị loại, che, băm hoặc mã hóa. Tên, số điện thoại và email thuộc nhóm không nhạy cảm nhưng vẫn có thể cần xử lý riêng; OWASP gợi ý xóa, xáo trộn hoặc thay bằng mã khi không cần biết danh tính hoặc rủi ro quá lớn.
Trang "Logging Vocabulary" của OWASP (không ghi ngày, đọc 04/11/2026) khuyên khi xóa dữ liệu nhạy cảm chỉ ghi hành động và mã đối tượng, không chép nội dung.
Ghi bằng trigger, từ ứng dụng hay pgAudit
Có 3 cách ghi, mỗi cách thấy một phần. Trigger là hàm tự chạy khi dòng bị thêm, sửa hoặc xóa; trang "Trigger Functions" của PostgreSQL 18 liệt kê 4 sự kiện: INSERT, UPDATE, DELETE và TRUNCATE.
Trigger chạy theo sự kiện trên bảng nên bắt cả SQL chạy tay, nếu còn bật (suy ra từ cách trigger hoạt động).
Ghi từ ứng dụng biết người dùng và lý do nghiệp vụ, nhưng bỏ sót thao tác đi đường khác (nhận định của Click Star). pgAudit là extension ghi câu lệnh vào nhật ký của PostgreSQL.
Trang PGAudit của Supabase (cập nhật 05/10/2026) liệt kê các nhóm read, write, ddl, role và các nhóm khác, chỉ cấu hình được ở mức vai trò. README của pgAudit nói không kiểm toán tin cậy được siêu quản trị, và nhật ký pgAudit không gắn với giao dịch.

Ví dụ chạy được: bảng nhật ký, trigger và tên người thao tác
Click Star chạy thử mã dưới đây ngày 04/11/2026 trên PGlite 0.5.15 với dữ liệu giả, theo thứ tự các khối; chưa chạy trên Supabase.
create schema audit;
create table audit.change_log (
id bigint generated always as identity primary key,
occurred_at timestamptz not null default now(),
actor text not null,
action text not null check (action in ('INSERT', 'UPDATE', 'DELETE', 'TRUNCATE')),
table_name text not null,
record_id text,
old_data jsonb,
new_data jsonb
);
create index change_log_record_idx on audit.change_log (table_name, record_id, id);
create function audit.record_change() returns trigger
language plpgsql security definer set search_path = ''
as $$
begin
insert into audit.change_log (actor, action, table_name, record_id, old_data, new_data)
values (
coalesce(nullif(pg_catalog.current_setting('app.actor', true), ''), session_user),
tg_op, tg_table_name,
coalesce(pg_catalog.to_jsonb(new) ->> tg_argv[0], pg_catalog.to_jsonb(old) ->> tg_argv[0]),
case when tg_op in ('UPDATE', 'DELETE') then pg_catalog.to_jsonb(old) - tg_argv[1:] end,
case when tg_op in ('INSERT', 'UPDATE') then pg_catalog.to_jsonb(new) - tg_argv[1:] end
);
return null;
end;
$$;
create table public.customers (
id bigint generated always as identity primary key,
name text not null, phone text, tier text not null default 'standard'
);
create trigger customers_audit
after insert or update or delete on public.customers
for each row execute function audit.record_change('id', 'phone');
create trigger customers_audit_truncate
after truncate on public.customers
for each statement execute function audit.record_change('id', 'phone');
Hàm dùng security definer để vai trò ứng dụng không cần quyền ghi vào bảng nhật ký; trang "Database functions" của Supabase (cập nhật 29/10/2026) yêu cầu khi đó đặt search_path, nên mã đặt search_path = ''.
Tham số đầu 'id' là tên cột khóa; bảng khóa theo code phải truyền tên đó. Bảng khóa phức hợp cần sửa hàm, nếu không record_id là NULL (đã chạy thử). Các tham số sau, ở đây 'phone', là cột bị bỏ khỏi bản sao. Khi xóa, hàm vẫn ghi dòng cũ trừ các cột này, nên với bảng nhạy cảm hãy liệt kê hết cột nhạy cảm.
Trigger thứ hai bắt TRUNCATE, lệnh trigger theo dòng không thấy. Chạy thử, truncate public.customers tạo 1 dòng TRUNCATE không có record_id.
Người thao tác lấy từ app.actor, thiếu thì ghi session_user (vai trò cơ sở dữ liệu). Theo mục 9.28 "System Administration Functions" của PostgreSQL 18 (không ghi ngày, đọc 04/11/2026), tham số true của set_config chỉ có hiệu lực trong giao dịch hiện tại; chạy thử, ngoài giao dịch thì tên rơi về postgres.
begin;
select set_config('app.actor', 'nv-lan', true);
insert into public.customers (name, phone) values ('Khách A', '0900000001');
commit;
begin;
select set_config('app.actor', 'nv-minh', true);
update public.customers set tier = 'vip' where id = 1;
commit;
begin;
select set_config('app.actor', 'nv-minh', true);
delete from public.customers where id = 1;
commit;
select * from audit.change_log order by id;
Kết quả chạy thử: khách A được thêm bởi nv-lan, sửa bởi nv-minh, rồi bị xóa, cho 3 dòng có giá trị cũ và mới, không có phone.
Tên người thao tác do ứng dụng khai, cơ sở dữ liệu không xác minh: chạy thử, vai trò ứng dụng đặt app.actor là ceo-forged và nhật ký ghi đúng tên đó.
Làm sao để không ai sửa được nhật ký
Logging Cheat Sheet khuyên có cách phát hiện bản ghi bị sửa hoặc xóa và chép nhật ký sang nơi chỉ đọc sớm nhất có thể.
Lớp đầu là quyền hạn. Trang "Securing your API" của Supabase (cập nhật 22/10/2026) nói trên dự án có sẵn, bảng mới trong public nhận mặc định select, insert, update, delete, truncate và vài quyền khác cho anon, authenticated, service_role, dù Supabase đang đổi mặc định này.
Vì vậy hãy đặt bảng nhật ký ngoài public; chạy thử, vai trò ứng dụng không có quyền trên schema audit nên bị từ chối.
Lớp thứ hai là trigger chặn sửa. Chạy thử, ngay cả vai trò sở hữu bảng cũng bị chặn với thông báo "audit.change_log is append-only".
create function audit.block_edit() returns trigger language plpgsql
as $$ begin raise exception 'audit.change_log is append-only'; end; $$;
create trigger change_log_no_edit before update or delete on audit.change_log
for each row execute function audit.block_edit();
create trigger change_log_no_truncate before truncate on audit.change_log
for each statement execute function audit.block_edit();
Nhưng chủ bảng vẫn tắt được trigger. Tài liệu ALTER TABLE của PostgreSQL 18 (không ghi ngày, đọc 04/11/2026) nói phải sở hữu bảng mới dùng được lệnh này, gồm DISABLE TRIGGER. Chạy thử, tắt xong thì sửa được dòng nhật ký.
Cũng theo trang đó, trigger mặc định không chạy khi phiên ở chế độ replica; ENABLE ALWAYS bắt nó chạy cả khi đó. Chạy thử: chèn khách trong chế độ này tạo 0 dòng nhật ký, sau enable always trigger tạo 1 dòng.
Vì vậy lớp thứ ba và thứ tư nằm ngoài cơ sở dữ liệu: cảnh báo khi trigger bị tắt hoặc nhật ký ngừng ghi, và bản sao ở nơi chỉ đọc.

Giữ bao lâu và đọc thế nào khi có sự cố
Thời hạn giữ là câu hỏi pháp lý, Click Star không đưa con số. Logging Cheat Sheet nói không hủy nhật ký trước hạn và không giữ quá hạn.
Khi có sự cố, truy vấn theo table_name và record_id (đã có chỉ mục), so old_data với new_data để thấy trường nào đổi. Khung đo của Click Star (không lấy từ nguồn ngoài): tỷ lệ bảng nhạy cảm đã gắn trigger, thời gian trả lời "ai sửa dòng này" khi diễn tập, và số ngày từ lần cuối có dòng mới trên bảng thường đổi.
Điều này có nghĩa gì với doanh nghiệp Việt Nam
Click Star chưa tìm thấy số đo công khai về mức độ doanh nghiệp Việt Nam có nhật ký kiểm toán cho ứng dụng nội bộ. Về pháp lý, hãy hỏi luật sư.
Đánh giá của Click Star. Với CRM hay ứng dụng quản lý nội bộ, nên bắt đầu từ 3 đến 5 bảng chứa tiền, quyền hạn và thông tin khách. Trigger cùng bảng chỉ thêm đủ để trả lời "ai sửa", cộng 1 bước truyền tên người thao tác, vốn chỉ đáng tin bằng lớp ứng dụng.
Đánh đổi cần quyết sớm: trigger nằm trong cùng giao dịch nên nhật ký lỗi thì lệnh sửa cũng bị hủy (đã chạy thử). Logging Cheat Sheet khuyên lỗi nhật ký không được làm ứng dụng ngừng chạy, còn mục A09 khuyên giao dịch lỗi phải hoàn tác và mặc định từ chối (cách đặt cạnh nhau là của Click Star). Click Star nghiêng về hủy lệnh với bảng tiền và quyền.
Nhật ký cũng cần phân quyền xem vì chứa dữ liệu khách. Click Star có dịch vụ phần mềm theo yêu cầu, CRM và CDP, nên có lợi ích liên quan.
Việc nên làm: 3 nhóm, 10 việc
Đây là khung tham khảo của Click Star, không lấy từ nguồn ngoài.
Chọn (3 việc)
- Liệt kê 3 đến 5 bảng chứa tiền, quyền hạn hoặc thông tin khách.
- Quyết định trường nào không ghi hoặc phải che.
- Hỏi chuyên gia pháp lý về thời hạn giữ và việc ghi thao tác nhân viên.
Dựng (4 việc)
- Tạo schema riêng cho nhật ký, thu quyền của vai trò ứng dụng.
- Gắn trigger ghi nhật ký và trigger
TRUNCATElên các bảng đã chọn, khai đúng cột khóa. - Truyền tên người thao tác vào đầu mỗi giao dịch.
- Thêm trigger chặn sửa, xóa và
TRUNCATEbảng nhật ký.
Kiểm (3 việc)
- Thử sửa nhật ký bằng vai trò ứng dụng và SQL tay trên bản sao.
- Đặt cảnh báo khi trigger bị tắt hoặc bảng hay đổi không có dòng mới.
- Diễn tập trả lời "ai sửa dòng này" và ghi thời gian.

Những sai lầm thường gặp
- Tin rằng nhật ký lỗi của máy chủ đủ để biết ai sửa dữ liệu.
- Gắn nhật ký cho mọi bảng, rồi không ai đọc vì quá nhiễu.
- Để bảng nhật ký trong schema mà vai trò ứng dụng sửa hoặc xóa được.
- Chép cả mật khẩu, token hoặc số thẻ vào giá trị cũ và mới.
- Ghi vai trò cơ sở dữ liệu thay vì nhân viên thật, nên mọi dòng đều mang cùng một tên.
- Chỉ thử khi mọi thứ đang chạy, không thử khi trigger bị tắt.
Điều chưa biết
Chưa có số về dung lượng nhật ký theo khối lượng sửa. Click Star chưa đọc văn bản pháp luật Việt Nam về thời hạn lưu nhật ký.
Cách lấy tên người dùng từ hệ thống đăng nhập của Supabase, khả năng giả mạo của nó và việc thu quyền trên Supabase thật chưa được chạy thử.
Bài liên quan
Cần tư vấn cụ thể cho doanh nghiệp?
Click Star giúp anh/chị xây dựng giải pháp phù hợp ngành nghề và quy mô. Trả lời mọi câu hỏi trong giờ làm việc.



