espace-paie-odentas/SECURITY_AUDIT_REPORT_NOVEMBER_2025.md
odentas 65d367cb5f fix: Supprimer endpoints pdf-proxy et pdf-clean avec CORS ouvert
- 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
2025-11-14 19:56:37 +01:00

29 KiB
Raw Permalink Blame History

🔒 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-report pour collecter les violations
  • Table csp_reports en 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-proxy et /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_me cookie

⚠️ 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 :

  1. Table Supabase : csp_reports

    • Stockage de toutes les violations CSP
    • Index sur directives, URIs bloquées, dates
    • RLS activé avec politiques staff
  2. Endpoint de collecte : /api/csp-report

    • Reçoit les violations du navigateur
    • Log console pour debug immédiat
    • Enregistrement en base de données
  3. 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
  4. 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, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
    .replace(/\//g, '&#x2F;');
}

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.local dans .gitignore
  • Séparation NEXT_PUBLIC_* vs variables privées
  • .env.example fourni

⚠️ 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)

  1. Implémenter CSP (Content Security Policy)

    • Impact:
    • Effort: 🔧🔧
    • Fichier: next.config.mjs
  2. Restreindre CORS sur /api/pdf-proxy et /api/pdf-clean

    • Impact:
    • Effort: 🔧
    • Fichiers: app/api/pdf-proxy/route.ts, app/api/pdf-clean/route.ts
  3. Ajouter rate limiting sur endpoints d'authentification

    • Impact:
    • Effort: 🔧🔧🔧
    • Fichier: middleware.ts

🟠 PRIORITÉ ÉLEVÉE (2-4 semaines)

  1. Sanitizer les inputs utilisateur (XSS)

    • Impact:
    • Effort: 🔧🔧🔧
    • Action: Installer DOMPurify, sanitizer tous les champs texte
  2. Chiffrer les données sensibles (NIR, IBAN)

    • Impact:
    • Effort: 🔧🔧🔧🔧
    • Action: Implémenter AES-256-GCM, migration des données
  3. Audit Trail complet

    • Impact:
    • Effort: 🔧🔧🔧
    • Action: Créer table audit_logs, logger toutes les actions critiques

🟡 PRIORITÉ MOYENNE (1-2 mois)

  1. Valider les fichiers uploadés (magic numbers)

    • Impact:
    • Effort: 🔧🔧
    • Action: Vérifier contenu réel avec file-type
  2. Implémenter rotation automatique des secrets

    • Impact:
    • Effort: 🔧🔧🔧🔧
    • Action: Scripts de rotation, alertes
  3. Monitoring avec Sentry

    • Impact:
    • Effort: 🔧🔧
    • Action: Installation, configuration

🟢 PRIORITÉ BASSE (2-3 mois)

  1. Endpoints RGPD (export, suppression)

    • Impact:
    • Effort: 🔧🔧🔧
    • Action: Routes /api/gdpr/*
  2. 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