Sécurité
Vue d'ensemble
La sécurité de MomentsCollectifs repose sur plusieurs couches :
- Transport : HTTPS/TLS
- Authentification : Supabase Auth + JWT
- Autorisation : Row Level Security (RLS)
- Validation : Côté client et serveur
- 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