Aller au contenu principal

Sécurité

Vue d'ensemble

La sécurité de MomentsCollectifs repose sur plusieurs couches :

  1. Transport : HTTPS/TLS
  2. Authentification : Supabase Auth + JWT
  3. Autorisation : Row Level Security (RLS)
  4. Validation : Côté client et serveur
  5. Storage : URLs signées

Transport (HTTPS)

Configuration Traefik

# traefik.yml
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https

websecure:
address: ":443"

certificatesResolvers:
letsencrypt:
acme:
email: admin@momentscollectifs.fr
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web

En-têtes de Sécurité

# Middleware Traefik
http:
middlewares:
security-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"

Authentification

Supabase Auth

// Configuration Auth
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
autoRefreshToken: true,
persistSession: true,
detectSessionInUrl: true
}
});

JWT Tokens

Structure du token JWT Supabase :

{
"aud": "authenticated",
"exp": 1234567890,
"sub": "user-uuid",
"email": "user@example.com",
"role": "authenticated",
"app_metadata": {},
"user_metadata": {}
}

Refresh Token

// Rafraîchissement automatique
supabase.auth.onAuthStateChange((event, session) => {
if (event === 'TOKEN_REFRESHED') {
console.log('Token refreshed');
}
if (event === 'SIGNED_OUT') {
// Nettoyer le state
this.store.dispatch(new Logout());
}
});

Protection des Routes

// auth.guard.ts
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private supabase: SupabaseService, private router: Router) {}

async canActivate(): Promise<boolean> {
const { data: { session } } = await this.supabase.auth.getSession();

if (!session) {
this.router.navigate(['/connexion']);
return false;
}

return true;
}
}

Autorisation (RLS)

Principes

Row Level Security permet de définir des règles d'accès au niveau des lignes.

-- Activer RLS
ALTER TABLE product ENABLE ROW LEVEL SECURITY;
ALTER TABLE product_items ENABLE ROW LEVEL SECURITY;

Politique "Propriétaire"

-- L'utilisateur ne voit que ses propres événements
CREATE POLICY "Users can view own products"
ON product FOR SELECT
TO authenticated
USING (created_by = auth.uid());

Politique "Participant"

-- Un participant peut voir les items de son événement
CREATE POLICY "Participants can view event items"
ON product_items FOR SELECT
TO authenticated
USING (
EXISTS (
SELECT 1 FROM product
WHERE product.id = product_items.product_id
AND (
product.created_by = auth.uid()
OR product_items.participant_id = auth.uid()::text
)
)
);

Politique "Anonyme"

-- Les anonymes peuvent créer des messages
CREATE POLICY "Anonymous can submit messages"
ON product_items FOR INSERT
TO anon
WITH CHECK (true);

-- Mais ne peuvent pas modifier
-- (pas de politique UPDATE pour anon)

Validation

Frontend (Angular)

// Reactive Forms avec validation
this.messageForm = this.fb.group({
content: ['', [
Validators.required,
Validators.minLength(10),
Validators.maxLength(5000)
]],
participantName: ['', [
Validators.required,
Validators.minLength(2),
Validators.maxLength(100),
Validators.pattern(/^[a-zA-ZÀ-ÿ\s\-']+$/)
]]
});

Sanitization HTML

// Utiliser DOMPurify pour le HTML user-generated
import DOMPurify from 'dompurify';

sanitizeHtml(content: string): string {
return DOMPurify.sanitize(content, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 'span'],
ALLOWED_ATTR: ['style'],
ALLOWED_STYLE: ['color', 'background-color', 'text-align']
});
}

Backend (Edge Functions)

// Validation dans Edge Function
function validateInviteRequest(body: any): { valid: boolean; error?: string } {
if (!body.productId || typeof body.productId !== 'number') {
return { valid: false, error: 'Invalid productId' };
}

if (!Array.isArray(body.emails) || body.emails.length === 0) {
return { valid: false, error: 'Emails array required' };
}

const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
for (const email of body.emails) {
if (!emailRegex.test(email)) {
return { valid: false, error: `Invalid email: ${email}` };
}
}

return { valid: true };
}

Storage Sécurisé

Politiques Storage

-- Bucket video_dedicace
-- Upload : utilisateurs authentifiés uniquement
CREATE POLICY "Authenticated can upload"
ON storage.objects FOR INSERT
TO authenticated
WITH CHECK (
bucket_id = 'video_dedicace'
AND (storage.foldername(name))[1] LIKE 'video_%'
);

-- Lecture : via URLs signées uniquement
-- (pas de politique SELECT publique)

URLs Signées

// Génération d'URL signée (1 heure)
async getSecureVideoUrl(path: string): Promise<string> {
const { data, error } = await this.supabase.storage
.from('video_dedicace')
.createSignedUrl(path, 3600); // 1 heure

if (error) throw error;
return data.signedUrl;
}

Validation des Fichiers

// Vérification côté client
validateVideoFile(file: File): boolean {
// Types autorisés
const allowedTypes = ['video/webm', 'video/mp4', 'video/quicktime'];
if (!allowedTypes.includes(file.type)) {
return false;
}

// Taille max : 100MB
const maxSize = 100 * 1024 * 1024;
if (file.size > maxSize) {
return false;
}

return true;
}

Protection CSRF/XSS

CSRF

Supabase utilise des tokens JWT dans les headers (pas de cookies), ce qui protège naturellement contre CSRF.

XSS

// Angular échappe automatiquement dans les templates
// Pour le HTML dynamique, utiliser [innerHTML] avec sanitization

@Pipe({ name: 'safeHtml' })
export class SafeHtmlPipe implements PipeTransform {
constructor(private sanitizer: DomSanitizer) {}

transform(value: string): SafeHtml {
// Sanitizer Angular
return this.sanitizer.bypassSecurityTrustHtml(
DOMPurify.sanitize(value)
);
}
}

Rate Limiting

Supabase

Supabase applique des limites par défaut :

  • 100 requêtes/seconde par IP
  • Limites sur Auth (5 inscriptions/heure par IP)

Video Generator

// Express rate limiting
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 requêtes par fenêtre
message: 'Trop de requêtes, réessayez plus tard'
});

app.use('/create_presentation', limiter);

Secrets et Configuration

Variables d'Environnement

# Ne JAMAIS commiter
SUPABASE_SERVICE_KEY=eyJ... # Accès admin
STRIPE_API_KEY=sk_live_...
STRIPE_WEBHOOK_SECRET=whsec_...

# Peuvent être publiques (côté client)
SUPABASE_URL=https://xxx.supabase.co
SUPABASE_ANON_KEY=eyJ... # Limité par RLS
STRIPE_PUBLIC_KEY=pk_live_...

Gestion des Secrets

# Utiliser les secrets Docker
docker secret create supabase_key ./supabase_key.txt

# Ou variables d'environnement chiffrées
# via le provider cloud

Audit et Logs

Supabase Logs

-- Activer les logs d'audit
ALTER SYSTEM SET log_statement = 'all';

-- Voir les logs
SELECT * FROM postgres_logs
WHERE timestamp > NOW() - INTERVAL '1 hour';

Application Logs

// Service de logging
@Injectable()
export class LogService {
logSecurityEvent(event: string, details: any) {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
type: 'SECURITY',
event,
details,
userId: this.authService.currentUserId
}));

// Envoyer à PostHog ou autre
posthog.capture(event, details);
}
}

Checklist Sécurité

  • HTTPS forcé sur tous les endpoints
  • JWT tokens avec expiration courte (1h)
  • RLS activé sur toutes les tables
  • Validation côté client ET serveur
  • Sanitization du HTML user-generated
  • URLs signées pour les fichiers privés
  • Rate limiting sur les APIs critiques
  • Secrets dans les variables d'environnement
  • Logs de sécurité activés
  • Headers de sécurité configurés