From 92ba837db78a12fe39d2430382250b79120ff58b Mon Sep 17 00:00:00 2001 From: walter Date: Mon, 12 Oct 2026 14:35:06 +0330 Subject: [PATCH 1/2] fix(security): scope runtime retention privilege verification --- scripts/ops/test-postgres-retention.sh | 30 +++++++++++++++++++++++++- scripts/ops/verify-retention-role.sql | 5 ++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/scripts/ops/test-postgres-retention.sh b/scripts/ops/test-postgres-retention.sh index 6e6c4a7..9c346f5 100755 --- a/scripts/ops/test-postgres-retention.sh +++ b/scripts/ops/test-postgres-retention.sh @@ -40,9 +40,11 @@ CREATE TABLE public.webhook_events (id text PRIMARY KEY, status text NOT NULL, c CREATE TABLE public.audit_logs (id text PRIMARY KEY, created_at timestamptz NOT NULL); CREATE TABLE public._prisma_migrations (id text PRIMARY KEY); CREATE TABLE public.unrelated (id text PRIMARY KEY); +CREATE TABLE public.store_notification_recipients (id text PRIMARY KEY); GRANT USAGE ON SCHEMA public TO wctm_runtime; GRANT SELECT, INSERT, UPDATE ON public.telegram_link_tokens, public.stores, public.telegram_callback_references, public.webhook_events TO wctm_runtime; GRANT SELECT, INSERT ON public.telegram_search_references, public.audit_logs TO wctm_runtime; +GRANT SELECT, INSERT, DELETE ON public.store_notification_recipients TO wctm_runtime; INSERT INTO public.telegram_link_tokens VALUES ('old', now()-interval '2 days'), ('fresh', now()-interval '12 hours'); INSERT INTO public.stores VALUES ('old', 'hash', now()-interval '2 days'), ('fresh', 'hash', now()-interval '12 hours'); INSERT INTO public.telegram_callback_references VALUES ('old', now()-interval '2 days', 'cipher', 'hash'), ('fresh', now()-interval '12 hours', 'cipher', 'hash'); @@ -75,13 +77,39 @@ SELECT has_table_privilege('wctm_runtime','public.telegram_link_tokens','DELETE' has_table_privilege('wctm_runtime','public.audit_logs','UPDATE'), has_table_privilege('wctm_runtime','public.audit_logs','DELETE'), has_table_privilege('wctm_runtime','public.telegram_search_references','UPDATE'), + has_table_privilege('wctm_runtime','public.store_notification_recipients','DELETE'), has_table_privilege('wctm_retention','public.unrelated','DELETE'), has_table_privilege('wctm_retention','public._prisma_migrations','UPDATE'), has_schema_privilege('wctm_retention','public','CREATE'), has_database_privilege('wctm_retention','retention_test','TEMP'), (SELECT count(*) FROM pg_class WHERE relowner='wctm_retention'::regrole), (SELECT rolsuper OR rolcreatedb OR rolcreaterole OR rolbypassrls OR rolinherit FROM pg_roles WHERE rolname='wctm_retention');") -[[ $roles = 'f|f|f|f|f|f|f|f|0|f' ]] +[[ $roles = 'f|f|f|f|t|f|f|f|f|0|f' ]] + +# Recipient mappings need runtime DELETE, but each retention-table DELETE must fail verification. +for table in telegram_link_tokens stores telegram_callback_references telegram_search_references webhook_events audit_logs; do + docker exec "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -c "GRANT DELETE ON public.$table TO wctm_runtime" + if docker exec -i "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -f - "$workspace/unapproved-runtime-grant.log" 2>&1; then + echo >&2 'test failed: verifier accepted runtime DELETE on a retention table' + exit 1 + fi + docker exec "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -c "REVOKE DELETE ON public.$table FROM wctm_runtime" +done +docker exec "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -c 'GRANT UPDATE ON public.audit_logs TO wctm_runtime' +if docker exec -i "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -f - "$workspace/unapproved-runtime-grant.log" 2>&1; then + echo >&2 'test failed: verifier accepted runtime UPDATE on audit logs' + exit 1 +fi +docker exec "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -c 'REVOKE UPDATE ON public.audit_logs FROM wctm_runtime' +docker exec -i "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -f - "$workspace/role-after-revokes.log" +grep -q 'retention role: PASS' "$workspace/role-after-revokes.log" unset_path=$WCTM_RETENTION_PGPASS_FILE unset WCTM_RETENTION_PGPASS_FILE diff --git a/scripts/ops/verify-retention-role.sql b/scripts/ops/verify-retention-role.sql index 7a44cd0..b3da4ee 100644 --- a/scripts/ops/verify-retention-role.sql +++ b/scripts/ops/verify-retention-role.sql @@ -45,7 +45,10 @@ BEGIN (pg_catalog.has_table_privilege('wctm_retention', c.oid, 'INSERT') OR (pg_catalog.has_table_privilege('wctm_retention', c.oid, 'DELETE') AND c.relname NOT IN ('telegram_link_tokens', 'audit_logs')) OR - pg_catalog.has_table_privilege('wctm_runtime', c.oid, 'DELETE')) + (c.relname IN ( + 'telegram_link_tokens', 'stores', 'telegram_callback_references', + 'telegram_search_references', 'webhook_events', 'audit_logs' + ) AND pg_catalog.has_table_privilege('wctm_runtime', c.oid, 'DELETE'))) ) OR NOT pg_catalog.has_table_privilege('wctm_retention', 'public.telegram_link_tokens', 'DELETE') OR NOT pg_catalog.has_table_privilege('wctm_retention', 'public.audit_logs', 'DELETE') OR pg_catalog.has_table_privilege('wctm_retention', 'public._prisma_migrations', 'SELECT') OR From a6fe82f3263f35c26db815a27f57538c8d6db3cb Mon Sep 17 00:00:00 2001 From: walter Date: Mon, 12 Oct 2026 14:53:54 +0330 Subject: [PATCH 2/2] fix(security): reject audit column update grants --- scripts/ops/test-postgres-retention.sh | 15 +++++++++++++++ scripts/ops/verify-retention-role.sql | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/scripts/ops/test-postgres-retention.sh b/scripts/ops/test-postgres-retention.sh index 9c346f5..a4ba1ab 100755 --- a/scripts/ops/test-postgres-retention.sh +++ b/scripts/ops/test-postgres-retention.sh @@ -107,6 +107,21 @@ if docker exec -i "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d rete fi docker exec "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ -c 'REVOKE UPDATE ON public.audit_logs FROM wctm_runtime' + +# A column-only UPDATE grant must also violate audit immutability. +docker exec "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -c 'GRANT UPDATE (created_at) ON public.audit_logs TO wctm_runtime' +audit_column_privileges=$(docker exec "$container" psql -XAt -v ON_ERROR_STOP=1 -U postgres -d retention_test -c " +SELECT has_table_privilege('wctm_runtime','public.audit_logs','UPDATE'), + has_any_column_privilege('wctm_runtime','public.audit_logs','UPDATE');") +[[ $audit_column_privileges = 'f|t' ]] +if docker exec -i "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -f - "$workspace/unapproved-runtime-grant.log" 2>&1; then + echo >&2 'test failed: verifier accepted runtime column UPDATE on audit logs' + exit 1 +fi +docker exec "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ + -c 'REVOKE UPDATE (created_at) ON public.audit_logs FROM wctm_runtime' docker exec -i "$container" psql -X -q -v ON_ERROR_STOP=1 -U postgres -d retention_test \ -f - "$workspace/role-after-revokes.log" grep -q 'retention role: PASS' "$workspace/role-after-revokes.log" diff --git a/scripts/ops/verify-retention-role.sql b/scripts/ops/verify-retention-role.sql index b3da4ee..6d18b81 100644 --- a/scripts/ops/verify-retention-role.sql +++ b/scripts/ops/verify-retention-role.sql @@ -54,7 +54,7 @@ BEGIN pg_catalog.has_table_privilege('wctm_retention', 'public._prisma_migrations', 'SELECT') OR pg_catalog.has_table_privilege('wctm_retention', 'public._prisma_migrations', 'UPDATE') OR pg_catalog.has_table_privilege('wctm_retention', 'public._prisma_migrations', 'DELETE') OR - pg_catalog.has_table_privilege('wctm_runtime', 'public.audit_logs', 'UPDATE') THEN + pg_catalog.has_any_column_privilege('wctm_runtime', 'public.audit_logs', 'UPDATE') THEN RAISE EXCEPTION 'retention table allowlist or runtime immutability failed'; END IF; IF EXISTS (