Durcit la sécurité : plafond de score réaliste, REVOKE explicites, banque de questions du Char réservée aux juges
Build and deploy / deploy (push) Successful in 36s
Build and deploy / deploy (push) Successful in 36s
Un score falsifié envoyé directement à submit_icarus_score/submit_melon_score pouvait se convertir en vraies gloires au Tribunal (plafond abaissé de 1 000 000 à 5000, contrainte + RPC). Les fonctions cron-only award_*_points_if_due() n'étaient protégées que par un commentaire, pas un vrai REVOKE. Le bucket avatars n'avait aucune limite de taille/type. La banque complète des questions du Char (spoiler du jeu) était lisible par tout Citoyen via /char ou un appel direct — RLS restreinte aux juges, /char passe désormais par la RPC chariot_revealed_question(). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+97
-11
@@ -56,6 +56,21 @@ alter table public.profiles enable row level security;
|
||||
-- trigger handle_new_user() (SECURITY DEFINER, contourne la RLS).
|
||||
grant select, update on public.profiles to authenticated;
|
||||
|
||||
-- 1bis. Durcissement de sécurité (audit) --------------------------------------
|
||||
-- PostgreSQL accorde EXECUTE à PUBLIC par défaut sur toute fonction créée,
|
||||
-- sauf révocation explicite. Plusieurs RPC de ce fichier n'étaient
|
||||
-- "protégées" que par un commentaire ("pas de grant à authenticated") sans
|
||||
-- REVOKE réel — donc en réalité appelables par n'importe quel utilisateur
|
||||
-- authentifié via l'API REST, malgré l'intention. Cette ligne change le
|
||||
-- comportement par défaut pour TOUTE fonction créée après elle dans ce
|
||||
-- script (donc pour toutes les fonctions ci-dessous) : plus aucune RPC
|
||||
-- n'est exécutable sans un `grant execute` explicite. Les RPC déjà
|
||||
-- existantes avant cette ligne conservent leurs anciens privilèges tant
|
||||
-- qu'on ne les révoque pas explicitement — voir les REVOKE ciblés plus bas
|
||||
-- pour award_icarus_points_if_due()/award_melon_points_if_due(), qui en
|
||||
-- avaient besoin rétroactivement.
|
||||
alter default privileges in schema public revoke execute on functions from public;
|
||||
|
||||
-- 2. Fonctions & triggers -------------------------------------------------
|
||||
-- La RLS est au niveau ligne : elle ne peut pas exprimer "cette colonne
|
||||
-- seulement si tel rôle". On verrouille donc les colonnes sensibles
|
||||
@@ -202,9 +217,17 @@ create policy "Judges can update any profile"
|
||||
|
||||
-- 4. Bucket avatars -------------------------------------------------------
|
||||
|
||||
insert into storage.buckets (id, name, public)
|
||||
values ('avatars', 'avatars', true)
|
||||
on conflict (id) do nothing;
|
||||
-- file_size_limit/allowed_mime_types ajoutés (audit de sécurité) : sans
|
||||
-- ça, un appel direct à l'API de Storage (hors de l'app, où l'upload est
|
||||
-- toujours compressé côté client via cropImageToBlob) pouvait uploader un
|
||||
-- fichier arbitrairement gros ou non-image comme "avatar" — même
|
||||
-- principe que sur le bucket wall-images.
|
||||
insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)
|
||||
values ('avatars', 'avatars', true, 3145728, array['image/webp', 'image/jpeg', 'image/png'])
|
||||
on conflict (id) do update set
|
||||
public = excluded.public,
|
||||
file_size_limit = excluded.file_size_limit,
|
||||
allowed_mime_types = excluded.allowed_mime_types;
|
||||
|
||||
-- Le bucket public sert la lecture via l'URL publique (hors RLS), mais une
|
||||
-- policy SELECT reste nécessaire : pour un upload en upsert (remplacement
|
||||
@@ -524,6 +547,16 @@ create table if not exists public.icarus_scores (
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- Plafond abaissé (audit de sécurité) : 1 000 000 était un score
|
||||
-- absurdement irréaliste, ce qui rendait triviale la soumission d'un
|
||||
-- score inventé directement via l'API pour se faire attribuer des gloires
|
||||
-- au Tribunal — voir la contrainte identique sur melon_scores. La
|
||||
-- contrainte inline de create table ne s'applique qu'à la création ; sur
|
||||
-- une table déjà existante il faut explicitement la remplacer.
|
||||
alter table public.icarus_scores drop constraint if exists icarus_scores_best_score_check;
|
||||
alter table public.icarus_scores add constraint icarus_scores_best_score_check
|
||||
check (best_score between 0 and 5000);
|
||||
|
||||
alter table public.icarus_scores enable row level security;
|
||||
|
||||
-- Même modèle que points_log/chariot_runs : verrouillée en écriture, seule
|
||||
@@ -558,7 +591,11 @@ begin
|
||||
raise exception 'authentication required';
|
||||
end if;
|
||||
|
||||
if p_score is null or p_score < 0 or p_score > 1000000 then
|
||||
-- Plafond réaliste (5000, largement au-delà de ce qu'une vraie partie
|
||||
-- peut atteindre), pas juste borné à 1 000 000 — voir la contrainte de
|
||||
-- table associée (best_score_check), la vraie garantie ; cette
|
||||
-- vérification donne juste un message d'erreur clair côté client.
|
||||
if p_score is null or p_score < 0 or p_score > 5000 then
|
||||
raise exception 'invalid score';
|
||||
end if;
|
||||
|
||||
@@ -593,8 +630,11 @@ end $$;
|
||||
-- Attribue les gloires du top 3 (ex-aequo inclus au même rang) dès que la
|
||||
-- date du Tribunal est atteinte ; no-op tant qu'elle n'est pas encore
|
||||
-- passée, et no-op définitif une fois déjà fait (icarus_points_awarded).
|
||||
-- Volontairement pas de grant execute à authenticated : uniquement appelée
|
||||
-- par pg_cron ou depuis le SQL Editor.
|
||||
-- Uniquement appelée par pg_cron ou depuis le SQL Editor : REVOKE explicite
|
||||
-- ci-dessous (ne pas se contenter de "ne pas accorder à authenticated" —
|
||||
-- PostgreSQL accorde EXECUTE à PUBLIC par défaut, donc sans révocation
|
||||
-- explicite cette fonction restait appelable par n'importe qui via l'API
|
||||
-- REST malgré l'intention).
|
||||
create or replace function public.award_icarus_points_if_due()
|
||||
returns void
|
||||
language plpgsql
|
||||
@@ -632,6 +672,8 @@ begin
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke execute on function public.award_icarus_points_if_due() from public, anon, authenticated;
|
||||
|
||||
-- ⚠️ pg_cron doit être activé une fois pour toutes via le Dashboard Supabase
|
||||
-- (Database → Extensions → "pg_cron" → Enable) — pas scriptable depuis ce
|
||||
-- fichier, et pas garanti self-service selon le plan/la région du projet.
|
||||
@@ -676,9 +718,18 @@ create table if not exists public.chariot_questions (
|
||||
alter table public.chariot_questions enable row level security;
|
||||
grant select, insert, update, delete on public.chariot_questions to authenticated;
|
||||
|
||||
-- Lecture réservée aux juges (audit de sécurité) : la banque complète est un
|
||||
-- spoiler du jeu en cours pour les Citoyens tant qu'une question n'a pas été
|
||||
-- révélée par un Archonte. /char (accessible à tous) n'a plus le droit de
|
||||
-- lire cette table directement — il passe par la RPC
|
||||
-- chariot_revealed_question() ci-dessous, qui ne renvoie que la question
|
||||
-- actuellement révélée. Seul /char/questions (déjà réservé aux juges) lit
|
||||
-- encore cette table directement.
|
||||
drop policy if exists "chariot_questions viewable by authenticated" on public.chariot_questions;
|
||||
create policy "chariot_questions viewable by authenticated"
|
||||
on public.chariot_questions for select to authenticated using (true);
|
||||
drop policy if exists "chariot_questions viewable by judges" on public.chariot_questions;
|
||||
create policy "chariot_questions viewable by judges"
|
||||
on public.chariot_questions for select to authenticated
|
||||
using (exists (select 1 from public.profiles p where p.id = auth.uid() and p.role = 'judge'));
|
||||
|
||||
drop policy if exists "chariot_questions insert by judges" on public.chariot_questions;
|
||||
create policy "chariot_questions insert by judges" on public.chariot_questions for insert to authenticated
|
||||
@@ -700,6 +751,28 @@ exception
|
||||
when duplicate_object then null;
|
||||
end $$;
|
||||
|
||||
-- Seul moyen pour un non-juge de savoir quelle question est actuellement
|
||||
-- révélée sans jamais lire la banque complète (RLS ci-dessus, juges
|
||||
-- uniquement) : renvoie zéro ligne tant qu'aucune question n'est révélée.
|
||||
-- question_number est calculé sur l'ordre complet (position, created_at)
|
||||
-- pour afficher "Question N" sur /char sans exposer les autres lignes.
|
||||
create or replace function public.chariot_revealed_question()
|
||||
returns table (id uuid, text text, question_number integer)
|
||||
language sql
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select numbered.id, numbered.text, numbered.question_number
|
||||
from (
|
||||
select q.id, q.text,
|
||||
row_number() over (order by q.position, q.created_at)::integer as question_number
|
||||
from public.chariot_questions q
|
||||
) numbered
|
||||
where numbered.id = (select s.chariot_revealed_question_id from public.settings s where s.id = true);
|
||||
$$;
|
||||
|
||||
grant execute on function public.chariot_revealed_question() to authenticated;
|
||||
|
||||
-- Nettoyage de l'ancien modèle par statut (team_a/team_b/out), remplacé par
|
||||
-- un modèle par emplacement fixe ci-dessous — schéma jamais appliqué en
|
||||
-- prod (retour utilisateur avant la première vraie migration), DROP direct.
|
||||
@@ -1435,6 +1508,12 @@ create table if not exists public.melon_scores (
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
-- Plafond abaissé (audit de sécurité) : voir la remarque identique sur
|
||||
-- icarus_scores.
|
||||
alter table public.melon_scores drop constraint if exists melon_scores_best_score_check;
|
||||
alter table public.melon_scores add constraint melon_scores_best_score_check
|
||||
check (best_score between 0 and 5000);
|
||||
|
||||
alter table public.melon_scores enable row level security;
|
||||
|
||||
-- Même modèle que icarus_scores : verrouillée en écriture, seule la RPC
|
||||
@@ -1469,7 +1548,11 @@ begin
|
||||
raise exception 'authentication required';
|
||||
end if;
|
||||
|
||||
if p_score is null or p_score < 0 or p_score > 1000000 then
|
||||
-- Plafond réaliste (5000, largement au-delà de ce qu'une vraie partie
|
||||
-- peut atteindre), pas juste borné à 1 000 000 — voir la contrainte de
|
||||
-- table associée (best_score_check), la vraie garantie ; cette
|
||||
-- vérification donne juste un message d'erreur clair côté client.
|
||||
if p_score is null or p_score < 0 or p_score > 5000 then
|
||||
raise exception 'invalid score';
|
||||
end if;
|
||||
|
||||
@@ -1504,8 +1587,9 @@ end $$;
|
||||
-- Attribue les gloires du top 3 (ex-aequo inclus au même rang) dès que la
|
||||
-- date du Tribunal est atteinte ; no-op tant qu'elle n'est pas encore
|
||||
-- passée, et no-op définitif une fois déjà fait (melon_points_awarded).
|
||||
-- Volontairement pas de grant execute à authenticated : uniquement appelée
|
||||
-- par pg_cron ou depuis le SQL Editor.
|
||||
-- Uniquement appelée par pg_cron ou depuis le SQL Editor : REVOKE explicite
|
||||
-- ci-dessous (voir la même remarque pour award_icarus_points_if_due —
|
||||
-- PostgreSQL accorde EXECUTE à PUBLIC par défaut sans révocation explicite).
|
||||
create or replace function public.award_melon_points_if_due()
|
||||
returns void
|
||||
language plpgsql
|
||||
@@ -1543,6 +1627,8 @@ begin
|
||||
end;
|
||||
$$;
|
||||
|
||||
revoke execute on function public.award_melon_points_if_due() from public, anon, authenticated;
|
||||
|
||||
-- ⚠️ pg_cron doit être activé une fois pour toutes via le Dashboard Supabase
|
||||
-- (Database → Extensions → "pg_cron" → Enable) — voir la même remarque à la
|
||||
-- section Icare. Les deux blocs ci-dessous n'échouent jamais bruyamment si
|
||||
|
||||
Reference in New Issue
Block a user