Skip to content

chore(security): fix audit_log service_role client (H3 createServerClient leak) #192

Description

@thierryvm

Contexte

Lors du smoke test post-PR #191 (fix 503 reste-à-vivre), l'insert audit_log pour l'event workspace.reste_a_vivre_updated a échoué avec permission denied for table audit_log. Bug isolé après la cause racine du 503 (workspace_members.order column rename).

Non-bloquant : logAuditEvent() est fire-and-forget avec catch (src/lib/security/audit-log.ts:126-129). L'app fonctionne, seul l'audit trail est cassé.

Hors scope de la PR security hardening (function grants + search_path) — déféré ici pour traitement dédié.

Trois hypothèses cause

H1 — Grant insert manquant pour service_role

Probabilité : faible — La table audit_log n'a aucun revoke explicite sur service_role ou postgres (vérifié dans toutes les migrations). service_role est owner-équivalent sur Supabase.

À vérifier (psql via Supabase MCP) :
```sql
select grantee, privilege_type, is_grantable
from information_schema.role_table_grants
where table_schema = 'public' and table_name = 'audit_log'
order by grantee, privilege_type;
```

H2 — RLS policy insert bloque

Probabilité : faibleservice_role a bypassrls par défaut sur Supabase. Les policies audit_log_no_insert (migration 20260417000003) ciblent to anon, authenticated, pas service_role.

À vérifier :
```sql
select rolname, rolbypassrls from pg_roles where rolname = 'service_role';
select * from pg_policies where tablename = 'audit_log';
```

H3 — `createAdminClient()` mauvais client (PISTE FORTE)

Probabilité : forte. Vérifié dans `src/lib/supabase/server.ts:37-54` :

```ts
export async function createAdminClient() {
const cookieStore = await cookies();
return createServerClient(env.NEXT_PUBLIC_SUPABASE_URL, env.SUPABASE_SERVICE_ROLE_KEY, {
cookies: {
getAll() { return cookieStore.getAll(); }, // Lit les cookies user
setAll() { /* noop */ },
},
auth: { autoRefreshToken: false, persistSession: false },
});
}
```

Problème : `createServerClient` de `@supabase/ssr` est conçu pour anon key + cookies user. Combiner `SERVICE_ROLE_KEY` + cookie handler qui lit les cookies user peut produire un comportement ambigu — selon la version du SDK, l'`Authorization` header peut être substitué par le JWT user présent en cookie, ce qui dégrade le client en rôle `authenticated`. Or les policies deny anon/authenticated bloquent alors l'insert.

Pattern canonique service_role = `createClient` de `@supabase/supabase-js` direct (sans SSR, sans cookies) :

```ts
import { createClient } from '@supabase/supabase-js';

export function createServiceRoleClient() {
return createClient(
env.NEXT_PUBLIC_SUPABASE_URL,
env.SUPABASE_SERVICE_ROLE_KEY,
{ auth: { autoRefreshToken: false, persistSession: false, detectSessionInUrl: false } },
);
}
```

Procédure d'investigation recommandée

  1. Confirmer H3 : ajouter un log temporaire dans `logAuditEvent` pour capturer `current_user` et `current_setting('request.jwt.claims', true)` avant l'insert (wrapper RPC `whoami_audit_probe` ou query directe).

  2. Si H3 confirmée : créer `createServiceRoleClient` direct, migrer les call-sites de `createAdminClient` un par un en validant :

    • `src/lib/security/audit-log.ts` (logAuditEvent)
    • `src/lib/auth/require-admin.ts` (admin RBAC check)
    • `src/lib/gdpr/deletion.ts`
    • `src/lib/gdpr/export.ts`
    • `src/lib/gdpr/consent.ts`
  3. Tests obligatoires :

    • Re-smoke reste-à-vivre → audit_log insert doit réussir (event `workspace.reste_a_vivre_updated`)
    • GDPR export/deletion flow → ne pas casser
    • Admin RBAC check → ne pas casser

Impact

  • Audit trail incomplet depuis l'introduction de `logAuditEvent` (peut affecter conformité GDPR art. 5(2) accountability + art. 30 record of processing si étendu à d'autres events)
  • Pas d'impact fonctionnel utilisateur (fire-and-forget)

Critères de done

  • H1/H2/H3 confirmées/réfutées avec preuves SQL
  • Fix appliqué (TS si H3, SQL si H1/H2 — peu probable)
  • Tous les call-sites de `createAdminClient` migrés et testés si H3
  • Re-smoke reste-à-vivre → audit_log insert OK
  • Tests régression GDPR + admin
  • Rapport `docs/prs/PR-audit-log-fix-report.md`

Références

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions