- Suppression de /app/api/pdf-proxy/route.ts (endpoint inutilisé avec CORS *) - Suppression de /app/api/pdf-clean/route.ts (endpoint inutilisé avec CORS *) - Mise à jour du rapport d'audit de sécurité - Les PDFs sont désormais affichés via URLs présignées S3 directes
29 KiB
🔒 Rapport d'Audit de Sécurité - Espace Paie Odentas
Date: 14 novembre 2025 (Mis à jour)
Projet: Nouvel Espace Paie Odentas
Type: Application Web SaaS Next.js 14 Full-Stack
Statut: 🟢 CSP Report-Only Activée
📊 Résumé Exécutif
🎯 Score Global de Sécurité: 8.0/10 ⬆️ (+0.5)
✅ Points Forts
- ✅ Authentification robuste avec Supabase Auth
- ✅ 2FA TOTP optionnel activable
- ✅ Row Level Security (RLS) activé sur la plupart des tables critiques
- ✅ Utilisation de Service Role Key pour contourner RLS côté serveur
- ✅ Rate limiting implémenté sur les endpoints critiques
- ✅ Cookies httpOnly pour la session
- ✅ Isolation par organisation (multi-tenant)
- ✅ Validation des permissions staff/organisation
- ✅ Hashing bcrypt pour les OTP (Odentas Sign)
- ✅ NOUVEAU: CSP Report-Only activée avec monitoring quotidien
⚠️ Points d'Attention Critiques
- <EFBFBD> EN COURS: CSP en mode Report-Only (activation complète prévue dans 5 jours)
🚀 Dernières Améliorations (14 Nov 2025)
- ✅ CSP Report-Only implémentée dans
next.config.mjs - ✅ Endpoint
/api/csp-reportpour collecter les violations - ✅ Table
csp_reportsen base de données - ✅ Cron job quotidien (9h00) envoyant un rapport par email
- ✅ Headers de sécurité additionnels (X-Frame-Options, X-Content-Type-Options, etc.)
- ✅ Suppression des endpoints
/api/pdf-proxyet/api/pdf-clean(CORS ouvert)
🔐 1. Authentification & Autorisation
1.1 Système d'Authentification
✅ Points Positifs
// Utilisation correcte de Supabase Auth
import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";
// Middleware rafraîchit automatiquement la session
const supabase = createMiddlewareClient({ req, res });
const { data: { session } } = await supabase.auth.getSession();
Implémentation:
- ✅ Supabase Auth v2 (moderne et sécurisé)
- ✅ Authentification par email + mot de passe
- ✅ Magic Links supportés
- ✅ Refresh token automatique dans middleware
- ✅ Cookies httpOnly (protection XSS)
- ✅ Session persistante avec
remember_mecookie
⚠️ Points d'Amélioration
1. 2FA TOTP - Comparaison de statut incorrecte
// ❌ PROBLÈME ACTUEL
const hasMfa = factors?.totp && factors.totp.length > 0;
// ✅ RECOMMANDATION
const hasMfa = factors?.totp?.some(f => f.status === "verified");
2. Rate Limiting sur authentification manquant
// ⚠️ Endpoints critiques sans rate limiting:
// - /api/auth/signin-password
// - /api/auth/send-code
// - /api/auth/verify-code
// - /api/auth/mfa/verify
// ✅ RECOMMANDATION: Implémenter un rate limiter global
const authRateLimiter = new Map<string, { count: number; resetAt: number }>();
const MAX_AUTH_ATTEMPTS = 5; // 5 tentatives
const WINDOW = 15 * 60 * 1000; // 15 minutes
3. Pas de protection contre les attaques par timing
// ⚠️ Actuellement
if (verifyError) {
return NextResponse.json({ error: "Code 2FA invalide" }, { status: 401 });
}
// ✅ RECOMMANDATION: Délai constant
async function verifyMfaWithConstantTime(code: string, expected: string) {
const delay = Math.random() * 100 + 200; // 200-300ms
await new Promise(resolve => setTimeout(resolve, delay));
return crypto.timingSafeEqual(
Buffer.from(code),
Buffer.from(expected)
);
}
1.2 Autorisation & RLS (Row Level Security)
✅ Politiques RLS Actives
-- ✅ Tables avec RLS activé
ALTER TABLE email_logs ENABLE ROW LEVEL SECURITY;
ALTER TABLE auto_declaration_tokens ENABLE ROW LEVEL SECURITY;
ALTER TABLE avenants ENABLE ROW LEVEL SECURITY;
ALTER TABLE promo_banners ENABLE ROW LEVEL SECURITY;
-- ✅ Politiques correctes
CREATE POLICY "Staff can view all email logs" ON email_logs
FOR SELECT
USING (
EXISTS (
SELECT 1 FROM staff_users
WHERE staff_users.user_id = auth.uid()
AND staff_users.is_staff = true
)
);
⚠️ Problèmes RLS
1. Politique trop permissive (temporaire pour debug)
-- ⚠️ PROBLÈME: Accès total pour debug
CREATE POLICY "Authenticated users can view email logs" ON email_logs
FOR SELECT
TO authenticated
USING (true);
-- ✅ RECOMMANDATION: Restreindre aux organisations
CREATE POLICY "Users view their org email logs" ON email_logs
FOR SELECT
USING (
organization_id IN (
SELECT org_id FROM organization_members
WHERE user_id = auth.uid() AND revoked = false
)
);
2. Vérifier RLS sur toutes les tables sensibles
-- ⚠️ Tables à vérifier:
-- - cddu_contracts
-- - employees (salaries)
-- - payslips
-- - cotisations
-- - salary_transfers
-- - organizations
-- - organization_members
-- - documents
1.3 Gestion des Rôles
✅ Points Positifs
// Vérification staff correcte
const { data: staffData } = await supabase
.from("staff_users")
.select("is_staff")
.eq("user_id", session.user.id)
.maybeSingle();
const isStaff = !!staffData?.is_staff;
⚠️ Recommandations
- Implémenter des rôles plus granulaires (Admin, Manager, Viewer)
- Ajouter des permissions par ressource (RBAC)
- Audit trail des changements de rôles
🌐 2. Sécurité Réseau & API
2.1 Headers de Sécurité
✅ IMPLÉMENTÉ: CSP en Mode Report-Only (14 Nov 2025)
// ✅ ACTUELLEMENT ACTIF dans next.config.mjs
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Content-Security-Policy-Report-Only',
value: [
"default-src 'self'",
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://eu-assets.i.posthog.com https://eu.i.posthog.com",
"style-src 'self' 'unsafe-inline'",
"img-src 'self' data: blob: https: https://*.s3.eu-west-3.amazonaws.com",
"font-src 'self' data:",
"connect-src 'self' https://eu.i.posthog.com https://*.supabase.co wss://*.supabase.co https://*.s3.eu-west-3.amazonaws.com https://*.lambda-url.eu-west-3.on.aws https://api.pdfmonkey.io https://api.docuseal.com https://api.docuseal.eu",
"frame-ancestors 'none'",
"frame-src 'self' blob:",
"base-uri 'self'",
"form-action 'self'",
"media-src 'self' blob:",
"worker-src 'self' blob:",
"object-src 'none'",
"report-uri /api/csp-report",
"upgrade-insecure-requests"
].join('; ')
},
{
key: 'X-Frame-Options',
value: 'DENY'
},
{
key: 'X-Content-Type-Options',
value: 'nosniff'
},
{
key: 'Referrer-Policy',
value: 'strict-origin-when-cross-origin'
},
{
key: 'Permissions-Policy',
value: 'geolocation=(), microphone=(), camera=(), payment=()'
},
{
key: 'X-XSS-Protection',
value: '1; mode=block'
}
]
}
]
}
📊 Système de Monitoring CSP
Infrastructure mise en place :
-
Table Supabase :
csp_reports- Stockage de toutes les violations CSP
- Index sur directives, URIs bloquées, dates
- RLS activé avec politiques staff
-
Endpoint de collecte :
/api/csp-report- Reçoit les violations du navigateur
- Log console pour debug immédiat
- Enregistrement en base de données
-
Rapport quotidien automatisé
- Cron Vercel : tous les jours à 9h00
- Email envoyé à
paie@odentas.fr - Agrégation par directive violée
- Top 3 exemples par type de violation
-
Période d'observation
- Durée : 5 jours (19 novembre 2025)
- Mode : Report-Only (ne bloque rien)
- Objectif : Identifier toutes les sources légitimes
Timeline de migration :
14 Nov 2025 : ✅ Activation Report-Only + Monitoring
19 Nov 2025 : 📊 Revue des rapports collectés
19 Nov 2025 : 🔧 Ajustement de la CSP selon les violations
20 Nov 2025 : 🚀 Activation complète (mode Enforce)
❌ ANCIEN ÉTAT (avant 14 Nov): Pas de CSP
Impact du problème : Exposition aux attaques XSS
- ✅ RÉSOLU : CSP Report-Only active, enforcement prévu dans 5 jours
2.2 CORS (Cross-Origin Resource Sharing)
✅ RÉSOLU: Endpoints CORS ouverts supprimés
Problème initial : CORS ouvert (*) sur /api/pdf-proxy et /api/pdf-clean
Solution appliquée (14 Nov 2025) :
- ✅ Suppression de
/app/api/pdf-proxy/route.ts(endpoint inutilisé) - ✅ Suppression de
/app/api/pdf-clean/route.ts(endpoint inutilisé) - ✅ Les PDFs sont maintenant affichés via URLs présignées S3 directes
Anciens endpoints problématiques :
// ❌ SUPPRIMÉ: app/api/pdf-proxy/route.ts
// Avait: 'Access-Control-Allow-Origin': '*'
// ❌ SUPPRIMÉ: app/api/pdf-clean/route.ts
// Avait: 'Access-Control-Allow-Origin': '*'
Approche actuelle :
- Les iframes utilisent directement les URLs présignées S3
- Pas de proxy nécessaire
- CORS géré par AWS S3 avec configuration stricte
2.3 Rate Limiting
✅ Implémenté sur /api/salaries (POST)
// ✅ BON EXEMPLE
const rateLimitMap = new Map<string, { count: number; windowStart: number }>();
const RATE_LIMIT_MAX = 50; // 50 créations par heure
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 heure
const userRateLimit = rateLimitMap.get(user.id);
if (userRateLimit && userRateLimit.count >= RATE_LIMIT_MAX) {
return NextResponse.json({
error: 'rate_limit_exceeded',
message: `Limite de ${RATE_LIMIT_MAX} créations par heure atteinte.`
}, { status: 429 });
}
⚠️ Manquant sur:
/api/auth/*(tous les endpoints d'authentification)/api/contrats(création de contrats)/api/documents/upload/api/staff/*(certains endpoints staff)/api/webhooks/*(validation de signature manquante)
✅ RECOMMANDATION: Middleware global de rate limiting
// lib/rate-limiter.ts
import { NextRequest } from 'next/server';
interface RateLimitConfig {
max: number;
window: number; // en ms
}
const RATE_LIMITS: Record<string, RateLimitConfig> = {
'/api/auth/*': { max: 10, window: 15 * 60 * 1000 }, // 10 req/15min
'/api/contrats': { max: 100, window: 60 * 60 * 1000 }, // 100 req/h
'/api/salaries': { max: 50, window: 60 * 60 * 1000 }, // 50 req/h
default: { max: 200, window: 60 * 60 * 1000 } // 200 req/h
};
export class RateLimiter {
private store = new Map<string, { count: number; resetAt: number }>();
check(identifier: string, path: string): boolean {
const config = this.getConfig(path);
const now = Date.now();
const key = `${identifier}:${path}`;
const record = this.store.get(key);
if (!record || now > record.resetAt) {
this.store.set(key, {
count: 1,
resetAt: now + config.window
});
return true;
}
if (record.count >= config.max) {
return false;
}
record.count++;
return true;
}
private getConfig(path: string): RateLimitConfig {
for (const [pattern, config] of Object.entries(RATE_LIMITS)) {
if (path.match(pattern)) return config;
}
return RATE_LIMITS.default;
}
}
// middleware.ts
const rateLimiter = new RateLimiter();
export async function middleware(req: NextRequest) {
const identifier = req.ip || req.headers.get('x-forwarded-for') || 'unknown';
const path = req.nextUrl.pathname;
if (!rateLimiter.check(identifier, path)) {
return new NextResponse('Too Many Requests', {
status: 429,
headers: {
'Retry-After': '900' // 15 minutes
}
});
}
// ... reste du middleware
}
🛡️ 3. Protection contre les Vulnérabilités Courantes
3.1 Injection SQL
✅ Points Positifs
- Utilisation exclusive de Supabase ORM (protection native)
- Pas de requêtes SQL brutes concaténées
- Paramétrage correct des requêtes
// ✅ BON: Utilisation de l'ORM Supabase
const { data } = await supabase
.from('cddu_contracts')
.select('*')
.eq('id', contractId) // ✅ Paramétré
.single();
3.2 XSS (Cross-Site Scripting)
⚠️ Problèmes Identifiés
1. innerHTML utilisé sans sanitization
// ⚠️ TROUVÉ dans simulateur.html et simulateur-embed.html
document.getElementById('result').innerHTML = resultTable; // ❌ DANGEREUX
document.getElementById('detailTable').innerHTML = generateDetailTable(...); // ❌
// ✅ RECOMMANDATION: Utiliser DOMPurify
import DOMPurify from 'dompurify';
const clean = DOMPurify.sanitize(resultTable);
document.getElementById('result').innerHTML = clean;
2. Pas de sanitization sur les inputs utilisateur
// ⚠️ Champs acceptant du texte libre:
// - Notes de contrats
// - Messages de tickets
// - Informations d'organisation
// ✅ RECOMMANDATION: Ajouter DOMPurify
npm install dompurify @types/dompurify
// lib/sanitize.ts
import DOMPurify from 'dompurify';
export function sanitizeHtml(dirty: string): string {
return DOMPurify.sanitize(dirty, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p', 'br', 'ul', 'ol', 'li'],
ALLOWED_ATTR: []
});
}
export function sanitizeText(text: string): string {
return text
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/\//g, '/');
}
3.3 CSRF (Cross-Site Request Forgery)
✅ Protection Native Next.js
- Next.js protège automatiquement les POST/PUT/DELETE via le même domaine
- Cookies SameSite="lax" configurés
⚠️ Amélioration Recommandée
// Ajouter des tokens CSRF pour les actions critiques
// middleware.ts
export function generateCsrfToken(): string {
return crypto.randomBytes(32).toString('hex');
}
// Vérifier sur les routes sensibles
const csrfToken = req.headers.get('x-csrf-token');
const sessionToken = req.cookies.get('csrf-token');
if (!csrfToken || csrfToken !== sessionToken) {
return new NextResponse('CSRF validation failed', { status: 403 });
}
3.4 Sécurité des Fichiers Uploadés
✅ Points Positifs
// Validation du type MIME
const validTypes = ['application/pdf', 'image/jpeg', 'image/png'];
if (!validTypes.includes(file.type)) {
throw new Error('Type de fichier non autorisé');
}
⚠️ Améliorations
1. Validation côté serveur insuffisante
// ✅ RECOMMANDATION: Vérifier le contenu réel du fichier
import { fileTypeFromBuffer } from 'file-type';
async function validateFileUpload(buffer: Buffer, expectedType: string) {
// Vérifier la magic number (vrai type du fichier)
const fileType = await fileTypeFromBuffer(buffer);
if (!fileType || fileType.mime !== expectedType) {
throw new Error('Type de fichier invalide');
}
// Vérifier la taille
const MAX_SIZE = 10 * 1024 * 1024; // 10MB
if (buffer.length > MAX_SIZE) {
throw new Error('Fichier trop volumineux');
}
// Scanner antivirus recommandé pour production
// await scanWithClamAV(buffer);
return true;
}
2. Noms de fichiers non sanitizés
// ⚠️ ACTUELLEMENT
const s3Key = `documents/${organizationId}/${filename}`;
// ✅ RECOMMANDATION
import { sanitizeFilename } from '@/lib/odentas-sign/crypto';
const safeFilename = sanitizeFilename(filename);
const s3Key = `documents/${organizationId}/${Date.now()}-${safeFilename}`;
🔑 4. Gestion des Secrets & Variables d'Environnement
4.1 Variables d'Environnement
✅ Points Positifs
.env.localdans.gitignore- Séparation
NEXT_PUBLIC_*vs variables privées .env.examplefourni
⚠️ Problèmes Identifiés
1. Exposition potentielle dans les logs
// ⚠️ PROBLÈME: Variables en clair dans console.error
console.error("[Middleware] Supabase env manquantes", {
hasUrl: Boolean(supabaseUrl),
hasAnonKey: Boolean(supabaseAnonKey),
note: "Vérifie .env.local"
});
// ✅ RECOMMANDATION: Ne jamais logger les valeurs
console.error("[Middleware] Supabase env manquantes - vérifier configuration");
2. Validation des variables critiques manquante
// ✅ RECOMMANDATION: lib/env-validation.ts
const requiredEnvVars = [
'NEXT_PUBLIC_SUPABASE_URL',
'NEXT_PUBLIC_SUPABASE_ANON_KEY',
'SUPABASE_SERVICE_ROLE_KEY',
'AWS_ACCESS_KEY_ID',
'AWS_SECRET_ACCESS_KEY',
'PDFMONKEY_API_KEY',
'DOCUSEAL_TOKEN'
] as const;
export function validateEnvironment() {
const missing = requiredEnvVars.filter(key => !process.env[key]);
if (missing.length > 0) {
throw new Error(
`Variables d'environnement manquantes: ${missing.join(', ')}\n` +
`Vérifiez votre fichier .env.local`
);
}
}
// pages/api/health/route.ts
validateEnvironment(); // Vérifier au démarrage
4.2 Rotation des Secrets
⚠️ Recommandations
- Service Role Key: Rotation tous les 90 jours
- AWS Access Keys: Rotation tous les 90 jours
- API Keys tierces: Rotation selon les recommandations
- JWT Secrets: Rotation annuelle minimum
📝 5. Logging & Monitoring
5.1 Audit Trail
⚠️ Manquant
// ✅ RECOMMANDATION: Table audit_logs
CREATE TABLE audit_logs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
action TEXT NOT NULL, -- 'contract.create', 'user.delete', etc.
resource_type TEXT NOT NULL,
resource_id TEXT,
organization_id UUID REFERENCES organizations(id),
ip_address INET,
user_agent TEXT,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_org ON audit_logs(organization_id);
CREATE INDEX idx_audit_logs_action ON audit_logs(action);
CREATE INDEX idx_audit_logs_created ON audit_logs(created_at DESC);
-- lib/audit-logger.ts
export async function logAudit(params: {
userId: string;
action: string;
resourceType: string;
resourceId?: string;
organizationId?: string;
metadata?: Record<string, any>;
req: NextRequest;
}) {
const supabase = createSbServiceRole();
await supabase.from('audit_logs').insert({
user_id: params.userId,
action: params.action,
resource_type: params.resourceType,
resource_id: params.resourceId,
organization_id: params.organizationId,
ip_address: params.req.ip || params.req.headers.get('x-forwarded-for'),
user_agent: params.req.headers.get('user-agent'),
metadata: params.metadata
});
}
// Utilisation
await logAudit({
userId: session.user.id,
action: 'contract.delete',
resourceType: 'contract',
resourceId: contractId,
organizationId: activeOrgId,
metadata: { reason: 'bulk-delete' },
req
});
5.2 Monitoring des Erreurs
✅ Points Positifs
- PostHog implémenté pour analytics
- Logs serveur basiques
⚠️ Améliorations
// ✅ RECOMMANDATION: Ajouter Sentry ou similaire
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.SENTRY_DSN,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
beforeSend(event, hint) {
// Filtrer les données sensibles
if (event.request?.headers) {
delete event.request.headers['authorization'];
delete event.request.headers['cookie'];
}
return event;
}
});
🔐 6. Sécurité des Données
6.1 Données Sensibles
✅ Protection en Place
- NIR (Numéro de Sécurité Sociale): Stocké en BDD
- IBAN/BIC: Stocké en BDD
- Dates de naissance: Stockées en BDD
🔴 CRITIQUE: Pas de chiffrement au repos
// ✅ RECOMMANDATION: Chiffrer les données sensibles
import crypto from 'crypto';
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!; // 32 bytes
const ALGORITHM = 'aes-256-gcm';
export function encrypt(text: string): string {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv);
let encrypted = cipher.update(text, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
export function decrypt(encryptedData: string): string {
const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
const decipher = crypto.createDecipheriv(
ALGORITHM,
Buffer.from(ENCRYPTION_KEY, 'hex'),
Buffer.from(ivHex, 'hex')
);
decipher.setAuthTag(Buffer.from(authTagHex, 'hex'));
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
decrypted += decipher.final('utf8');
return decrypted;
}
// Migration
ALTER TABLE employees ADD COLUMN nir_encrypted TEXT;
ALTER TABLE employees ADD COLUMN iban_encrypted TEXT;
// Utilisation
const encryptedNir = encrypt(employee.nir);
await supabase
.from('employees')
.update({ nir_encrypted: encryptedNir })
.eq('id', employeeId);
6.2 RGPD & Confidentialité
✅ Points Positifs
- Consentement analytics (Cookie banner)
- Pages légales (Mentions légales, Politique de confidentialité)
- Accès limité par organisation
⚠️ Améliorations
- Droit à l'oubli: Implémenter une fonction de suppression complète
- Export des données: Permettre l'export RGPD
- Durée de rétention: Définir et appliquer des politiques de rétention
- Consentement explicite: Logger les consentements
// ✅ RECOMMANDATION: Endpoint RGPD
// app/api/gdpr/export/route.ts
export async function GET(req: NextRequest) {
const session = await getSession(req);
if (!session) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
const supabase = createSbServiceRole();
// Exporter toutes les données de l'utilisateur
const userData = await Promise.all([
supabase.from('profiles').select('*').eq('user_id', session.user.id),
supabase.from('organization_members').select('*').eq('user_id', session.user.id),
// ... autres tables
]);
return NextResponse.json({
user_id: session.user.id,
export_date: new Date().toISOString(),
data: userData
}, {
headers: {
'Content-Disposition': 'attachment; filename=my-data.json'
}
});
}
// app/api/gdpr/delete/route.ts
export async function DELETE(req: NextRequest) {
// Supprimer toutes les données + anonymisation
// Avec confirmation par email + période de grâce de 30 jours
}
🚀 7. Infrastructure & Déploiement
7.1 Configuration Vercel
✅ Points Positifs
// vercel.json
{
"regions": ["cdg1"], // ✅ Région Paris (RGPD compliant)
"functions": {
"app/api/**/*.ts": {
"maxDuration": 30 // ✅ Timeout raisonnable
}
}
}
⚠️ Recommandations
- Activer Vercel WAF (Web Application Firewall)
- Configurer les logs Vercel pour audit
- Activer les alertes de monitoring
7.2 Secrets dans Vercel
✅ Recommandations
# Utiliser Vercel Secrets pour les variables sensibles
vercel secrets add supabase-service-role-key "xxx"
vercel secrets add aws-secret-access-key "xxx"
vercel secrets add pdfmonkey-api-key "xxx"
# Ne jamais commit de secrets
echo ".env.local" >> .gitignore
📊 8. Plan d'Action Priorisé
🔴 PRIORITÉ CRITIQUE (0-2 semaines)
-
Implémenter CSP (Content Security Policy)
- Impact: ⭐⭐⭐⭐⭐
- Effort: 🔧🔧
- Fichier:
next.config.mjs
-
Restreindre CORS sur
/api/pdf-proxyet/api/pdf-clean- Impact: ⭐⭐⭐⭐⭐
- Effort: 🔧
- Fichiers:
app/api/pdf-proxy/route.ts,app/api/pdf-clean/route.ts
-
Ajouter rate limiting sur endpoints d'authentification
- Impact: ⭐⭐⭐⭐⭐
- Effort: 🔧🔧🔧
- Fichier:
middleware.ts
🟠 PRIORITÉ ÉLEVÉE (2-4 semaines)
-
Sanitizer les inputs utilisateur (XSS)
- Impact: ⭐⭐⭐⭐
- Effort: 🔧🔧🔧
- Action: Installer DOMPurify, sanitizer tous les champs texte
-
Chiffrer les données sensibles (NIR, IBAN)
- Impact: ⭐⭐⭐⭐
- Effort: 🔧🔧🔧🔧
- Action: Implémenter AES-256-GCM, migration des données
-
Audit Trail complet
- Impact: ⭐⭐⭐
- Effort: 🔧🔧🔧
- Action: Créer table
audit_logs, logger toutes les actions critiques
🟡 PRIORITÉ MOYENNE (1-2 mois)
-
Valider les fichiers uploadés (magic numbers)
- Impact: ⭐⭐⭐
- Effort: 🔧🔧
- Action: Vérifier contenu réel avec
file-type
-
Implémenter rotation automatique des secrets
- Impact: ⭐⭐⭐
- Effort: 🔧🔧🔧🔧
- Action: Scripts de rotation, alertes
-
Monitoring avec Sentry
- Impact: ⭐⭐⭐
- Effort: 🔧🔧
- Action: Installation, configuration
🟢 PRIORITÉ BASSE (2-3 mois)
-
Endpoints RGPD (export, suppression)
- Impact: ⭐⭐
- Effort: 🔧🔧🔧
- Action: Routes
/api/gdpr/*
-
Tests de sécurité automatisés
- Impact: ⭐⭐
- Effort: 🔧🔧🔧🔧
- Action: OWASP ZAP, tests de pénétration
📋 9. Checklist de Conformité SaaS
🔐 Authentification & Autorisation
- Authentification sécurisée (Supabase Auth)
- 2FA disponible
- Rate limiting sur auth (❌)
- RLS activé
- RLS vérifié sur toutes les tables (⚠️)
- Validation des permissions
- Rotation des secrets (⚠️)
🛡️ Protection des Données
- Chiffrement des données sensibles au repos (❌)
- HTTPS obligatoire
- Cookies httpOnly
- SameSite cookies
- Sanitization XSS (⚠️)
- Protection SQL Injection (ORM)
🌐 Sécurité Réseau
- CSP configuré (❌)
- CORS restreint (⚠️)
- Headers sécurité basiques
- Rate limiting global (⚠️)
- WAF activé (⚠️)
📊 Monitoring & Logging
- Audit trail complet (❌)
- Logs d'erreurs basiques
- Monitoring avancé (Sentry) (❌)
- Analytics (PostHog)
📜 Conformité RGPD
- Consentement cookies
- Politique de confidentialité
- Mentions légales
- Export données utilisateur (❌)
- Droit à l'oubli (❌)
- Registre des consentements (❌)
🚀 Infrastructure
- Région EU (cdg1)
- Secrets sécurisés (Vercel)
- Environnements séparés (dev/prod)
- Backups automatiques (⚠️)
- Plan de reprise d'activité (⚠️)
🎯 Score Détaillé par Catégorie
| Catégorie | Score | Commentaire |
|---|---|---|
| Authentification | 8/10 | ✅ Robuste mais rate limiting manquant |
| Autorisation (RLS) | 7/10 | ⚠️ Politiques à vérifier/durcir |
| Protection XSS | 5/10 | ⚠️ Sanitization manquante |
| Protection CSRF | 8/10 | ✅ Next.js natif + SameSite |
| Headers Sécurité | 3/10 | ❌ CSP manquant |
| CORS | 4/10 | ⚠️ Trop permissif sur certains endpoints |
| Rate Limiting | 5/10 | ⚠️ Partiel uniquement |
| Chiffrement Données | 4/10 | ⚠️ Pas de chiffrement au repos |
| Logging & Audit | 5/10 | ⚠️ Basique, audit trail manquant |
| RGPD | 6/10 | ⚠️ Bases OK, export/suppression manquants |
| Infrastructure | 8/10 | ✅ Bien configuré (cdg1, Vercel) |
SCORE GLOBAL: 7.5/10 ⭐⭐⭐⭐
📚 Ressources & Standards
Standards de Sécurité
Next.js Security
Supabase Security
RGPD
🔚 Conclusion
Votre application Espace Paie Odentas présente une base solide de sécurité avec :
- ✅ Authentification robuste (Supabase Auth + 2FA)
- ✅ Isolation multi-tenant correcte
- ✅ RLS activé sur les tables critiques
- ✅ Protection SQL injection native
Cependant, plusieurs améliorations critiques sont nécessaires pour atteindre les standards SaaS :
- 🔴 CSP manquant (exposition XSS)
- 🔴 CORS trop permissif (risque CSRF)
- 🔴 Données sensibles non chiffrées (NIR, IBAN)
- 🟠 Rate limiting insuffisant (attaques brute-force)
- 🟠 Sanitization XSS manquante (innerHTML dangereux)
Recommandation Finale: Implémenter le Plan d'Action Priorisé en commençant par les 3 actions critiques (CSP, CORS, Rate Limiting Auth) qui peuvent être réalisées en moins de 2 semaines et amélioreront significativement la posture de sécurité.
Rapport généré le: 14 novembre 2025
Par: GitHub Copilot - Audit de Sécurité
Version du projet: 0.1.0
Prochain audit recommandé: Février 2026