Audit de sécurité (revue statique du code)

Application : SFER · Stack : Laravel (PHP) · Périmètre : dossiers applicatifs (app/, routes/, config/, resources/views/), exclusions : vendor/ et dépendances tierces.
Date du rapport : 4 mai 2026 · Méthode : lecture du code et recherche de motifs sensibles (auth, redirections, CSP, uploads, SQL brut, exposition d’erreurs, chemins fichiers).

Légende des références : · C1… Critique · H1… Haute · M1… Moyenne · L1… Faible · I1… Informative / non score CVSS équivalent.
Lors de cet audit statique : aucune faille de gravité critique (Cn) n’a été retenue.

Suivi des correctifs : dans chaque fiche ci-dessous, le badge à droite du titre indique si le point est traité. Lorsqu’un correctif est mergé ou déployé, remplacez la balise correspondante :
<span class="fix-status fix-pending"> par <span class="fix-status fix-done">Corrigé le JJ/MM/AAAA</span>
(mettez également à jour la colonne Statut dans le tableau).

Glossaire des acronymes (référence)

ACL / RBAC
Access Control List / Role-Based Access Control : modèles de contrôle d’accès où des permissions ou rôles déterminent qui peut faire quoi dans l’application.
CSP
Content Security Policy (politique de sécurité du contenu) : en-tête HTTP qui dit au navigateur d’où il a le droit de charger scripts, styles, images, etc. Elle limite l’impact d’un script malveillant injecté (XSS).
CSRF
Cross-Site Request Forgery : forcage de requêtes depuis le navigateur d’une victime encore connectée. Laravel utilise en général des jetons pour s’en prémunir.
CVE / CVSS
Common Vulnerabilities and Exposures (identifiant public d’une vulnérabilité) / Common Vulnerability Scoring System (score de gravité).
JWT
JSON Web Token — non central dans ce rapport ; format de jeton souvent utilisé pour l’API stateless.
Open redirect · OWASP ZAP
Open redirect : redirection HTTP vers une destination partiellement ou totalement contrôlée par l’attaquant. · OWASP ZAP (Zed Attack Proxy) : outil open source de test de sécurité des applications web.
XSS
Cross-Site Scripting : injection de code (souvent JavaScript) exécuté dans le navigateur d’une autre personne, sous votre domaine ou via un contenu de confiance affiché sans échappement.

Résumé exécutif

L’application présente plusieurs bonnes pratiques (limitation du login admin, régénération de session après connexion, contrôle d’URL « intended » vers l’admin, purification HTML via directive Blade personnalisée, webhook Stripe signé avec option de filtrage IP, uploads TinyMCE restreints aux images sans SVG). Le point le plus préoccupant identifié était une redirection ouverte (H1) sur le changement de langue de l’administration — corrigée le 04/05/2026. La politique CSP globale est volontairement permissive (unsafe-inline / unsafe-eval), réf. M1, ce qui réduit la défense en profondeur contre le XSS. Les réponses incluant $e->getMessage() côté client ont été supprimées pour TinyMCEController et SettingsController (L1, corrigée le 06/05/2026 dans ce périmètre). Les clés d’édition du fichier sfer.php sont désormais validées de façon uniforme (M2, corrigée le 06/05/2026). La couche Stripe webhooks associe déjà signature HMAC et filtre IP configurable (L2), considérée comme corrigée (validation des mesures) le 06/05/2026, sous réserve d’actualiser ponctuellement la liste officielle Stripe dans config/stripe_webhooks.php.

Tableau des constats

Réf. Gravité Statut Sujet Fichier / zone
H1 Haute Corrigé le 04/05/2026 Redirection ouverte (paramètre redirect + url()->previous()) — traitée app/Http/Controllers/Admin/AdminLocaleController.php
M1 Moyenne À corriger CSP globale avec unsafe-inline et unsafe-eval app/Http/Middleware/SecurityHeaders.php
M2 Moyenne Corrigé le 06/05/2026 Validation des clés config/sfer.php (update / add / delete) harmonisée — traitée app/Http/Controllers/Admin/SettingsController.php
L1 Faible Corrigé le 06/05/2026 Messages d’erreur HTTP sans fuite de getMessage()TinyMCEController, SettingsController (autres contrôleurs possibles) app/Http/Controllers/Admin/TinyMCEController.php, SettingsController.php
L2 Faible Corrigé le 06/05/2026 config/stripe_webhooks.php, VerifyStripeWebhookSourceIp, StripeWebhookController
I1 Informative À traiter si besoin Surface d’admin « notifications / dashboard » sans middleware de module métier routes/admin.php
I2 Informative À traiter si besoin Référence à une vue publique inexistante pages.show app/Http/Controllers/PageController.php
I3 Informative À traiter si besoin Routes admin dynamiques issues des slugs « modules » en base routes/admin.php
I4 Informative À traiter si besoin Middleware CheckAdminModulePermission — permissions pour actions hors CRUD app/Http/Middleware/CheckAdminModulePermission.php

Détail des constats

H1 Haute Open redirect — changement de langue admin Corrigé le 04/05/2026

Correctif : la redirection après changement de langue n’accepte plus qu’une cible sous le préfixe /cp-admin, avec vérification d’hôte et de port lorsqu’une URL absolue est fournie ; les URL externes et les chemins contenant .. sont rejetés. Comportement aligné avec AdminLoginController::sendLoginResponse pour l’URL « intended ».

Ancienne exposition : usage de $request->query('redirect') et de url()->previous() passés à redirect()->to() sans filtrage strict (phishing ou autre destination externe possible).

Exemples d’URL qu’un attaquant pouvait proposer (avant correctif)

Route concernée : GET /cp-admin/set-language/{locale} (admin.set-language). Remplacez monsite.example par votre hôte réel.

  • Paramètre redirect vers un site tiers (clone de login) :
    https://monsite.example/cp-admin/set-language/fr?redirect=https://phishing-attaquant.example/faux-cp-admin
  • Même intention avec une URL codée :
    https://monsite.example/cp-admin/set-language/en?redirect=https%3A%2F%2Fevil.example%2Fcollecte
  • Redirection protocol-relative (//), souvent acceptée comme « hors du domaine légitime » sans filtre :
    https://monsite.example/cp-admin/set-language/fr?redirect=//malveillant.example/page

Une autre variante passait par l’en-tête Referer : la victime arrive d’abord depuis un lien externe, puis url()->previous() pouvait devenir cette origine ; après changement de langue, elle était encore redirigée vers le site hostile sans paramètre redirect dans l’URL affichée.

app/Http/Controllers/Admin/AdminLocaleController.php (méthode switch)
Acronymes & termes

URL (Uniform Resource Locator) : adresse web (schéma, hôte, chemin, paramètres). Open redirect : la requête aboutit à une redirection vers une destination partiellement ou totalement contrôlée par l’attaquant. Phishing : hameçonnage — tromper l’utilisateur pour qu’il divulgue des identifiants ou exécute une action.

Type d’attaque / scénario
  • Phishing par chaîne de confiance : e-mail ou message contenant un lien vers votre domaine (légitime) qui, après changement de langue, renvoie vers un clone du login admin volant la session ou les mots de passe.
  • Contournement de contrôles côté client : combinaison avec d’autres bugs (XSS, etc.) pour forcer une navigation vers un site malveillant tout en conservant l’illusion d’une action « officielle ».

Recommandation : n’autoriser que les chemins relatifs commençant par /cp-admin (sur le même principe que sendLoginResponse dans AdminLoginController), ou valider explicitement host + path.

M1 Moyenne Politique CSP permissive À corriger

SecurityHeaders définit une CSP avec script-src incluant 'unsafe-inline' et 'unsafe-eval'. C’est compréhensible pour TinyMCE, Tailwind CDN, etc., mais tout script inline reste autorisé : en cas de XSS, la mitigation navigateur est fortement diminuée.

app/Http/Middleware/SecurityHeaders.php — globalContentSecurityPolicy()
Acronymes & termes

CSP : voir glossaire. unsafe-inline : autorise les blocs et gestionnaires d’événements inline dans le HTML. unsafe-eval : autorise eval() et constructions équivalentes (ex. certains modèles de génération de code côté navigateur). XSS : injection de script exécuté dans le contexte du site (vol de cookie de session si non protégé, actions au nom de l’utilisateur, etc.).

Type d’attaque / scénario
  • XSS réfléchi ou stocké (si une autre entrée permet d’injecter du HTML/JS) exécuté dans le navigateur de la victime ; la CSP permissive empêche moins efficacement l’exécution du code injecté.
  • Vol de données de session ou d’interface admin via script malveillant chargé après compromission mineure (contenu, extension, fichier tiers compromis).

Recommandation : nonces ou hashes pour les scripts statiques ; routes CSP distinctes pour l’admin et le front ; éviter Tailwind CDN en production au profit d’un build.

M2 Moyenne Variables de configuration — validation asymétrique Corrigé le 06/05/2026

Correctif : la validation de la clé utilise désormais les mêmes règles pour updateConfigVariable, addConfigVariable et deleteConfigVariable, via une méthode privée commune (regex:/^[a-zA-Z0-9_.]+$/, longueur max 255).

Ancienne exposition : updateConfigVariable n’exigeait qu’une chaîne max:255, tandis que seul addConfigVariable appliquait la regex restrictive — risque de clés ou segments atypiques lors d’une mise à jour.

app/Http/Controllers/Admin/SettingsController.php
Acronymes & termes

Config applicative : fichier PHP lu au démarrage (ex. coordonnées société, paramètres métier). Ce n’est pas une CSRF en soi, mais la surface d’écriture fichier augmente l’impact d’un compte admin compromis.

Type d’attaque / scénario
  • Abus de session admin compromise ou compte trop privilégié : modification de clés sensibles dans sfer.php pour rediriger des e-mails, afficher du contenu malveillant, ou casser applicativement le site (disponibilité / intégrité).
  • Bug « logique métier » : clés mal formées pouvant produire une config invalide et des pannes jusqu’à fuite de trace d’erreur si mal gérées.

Recommandation : réutiliser la même contrainte de validation que pour addConfigVariable et, idéalement, une liste blanche des clés modifiables en production.

L1 Faible Exposition de messages d’exception Corrigé le 06/05/2026

Correctif (périmètre audit) : les blocs catch concernés appellent désormais report($e) pour journaliser l’erreur réelle et renvoient un message générique traduit (admin/settings.messages.errors.unexpected, path_not_allowed pour certains chemins invalide sous lang/, admin/tinymce.upload_failed pour l’upload).

Ancienne exposition : concaténation de $e->getMessage() dans des réponses JSON ou en session flash pouvait révéler chemins système ou détails d’infra.

app/Http/Controllers/Admin/TinyMCEController.php (catch)
app/Http/Controllers/Admin/SettingsController.php (plusieurs méthodes)
Acronymes & termes

JSON : format d’échange de données texte souvent utilisé par les API et les réponses AJAX. Information disclosure (fuite d’information) : exposer des détails internes utiles à un attaquant pour affiner une attaque (versions, chemins, requêtes SQL dans le message d’erreur, etc.).

Type d’attaque / scénario
  • Reconnaissance : un utilisateur authentifié (ou parfois non) déclenche volontairement des erreurs pour cartographier l’arborescence serveur ou la stack.
  • Chaînage avec d’autres vulnérabilités (chemins de fichiers pour path traversal, noms de tables, etc.).

Recommandation : journaliser en interne uniquement (report() / Log::error) et réponse générique au client hors environnement debug.

L2 Faible Webhooks Stripe et liste d’IP Corrigé le 06/05/2026 aucun défaut de conception supplémentaire n’a été requis : la route POST /stripe/webhook vérifie d’abord l’origine (stripe.webhook.ip + fichier config/stripe_webhooks.php), puis StripeWebhookController valide la charge utile avec Webhook::constructEvent et le secret de webhook (services.stripe.webhook_secret).

Le risque signalé était opérationnel (liste d’IP officielle évolutive) : le suivi passe par une resynchronisation périodique avec la documentation Stripe (ips_webhooks.json) et une surveillance des logs [Stripe Webhook].

config/stripe_webhooks.php · app/Http/Middleware/VerifyStripeWebhookSourceIp.php
Acronymes & termes

Webhook : appel HTTP serveur à serveur (ici Stripe → votre application) suite à un événement paiement. IP allowlist / liste blanche : n’accepte les requêtes que depuis des adresses IP connues comme appartenant au prestataire. DoS fonctionnel — ici indirect : paiements qui ne confirment plus si tout le trafic est rejeté.

Type d’attaque / scénario
  • Perte de disponibilité : changement des IP Stripe non reflété → webhooks rejetés → inscriptions non confirmées automatiquement.
  • Mauvaise configuration : désactivation de la vérif IP en production « pour que ça marche » sans renforcer le secret / la surveillance des logs — augmente la surface si un attaquant trouvait un moyen de falsifier la signature (peu probable si secret correct) ou en cas d’erreur de déploiement.

Pour la suite : garder STRIPE_WEBHOOK_VERIFY_IP=true en production et reporter dans ce fichier de config ou en procédure interne tout changement venant des plages Stripe.

I1 Informative Notifications et dashboard — périmètre d’accès À traiter si besoin

Ces routes sont protégées par admin.auth mais pas par le middleware admin.module.permission contrairement au gros du back-office. Tout administrateur authentifié y a donc accès, ce qui peut être voulu fonctionnellement mais élargit la surface par rapport aux modules métiers à permission explicite.

routes/admin.php — préfixes notifications, dashboard, logout, profil, 2FA
Acronymes & termes

Middleware (Laravel) : couche qui filtre la requête avant le contrôleur (ici authentification vs permissions fines). RBAC : voir glossaire — ici écart entre « tout admin » et « admin avec rôle module X ».

Type d’attaque / scénario
  • Élévation horizontale de privilèges métier : un compte admin limité (ex. contenu uniquement) accède quand même au tableau de bord ou aux notifications potentiellement sensibles selon leur contenu.
  • Pas nécessairement une « vulnérabilité » si le modèle de menace accepte tout staff authentifié dans cette zone ; à valider métier.

I2 Informative Vue publique pages.show absente du dépôt À traiter si besoin

PageController::show renvoie view('pages.show', …) alors qu’aucun fichier Blade correspondant n’apparaît sous resources/views/pages/ : risque d’erreur 500 lors de l’affichage d’une page CMS par slug.

app/Http/Controllers/PageController.php — méthode show()
Acronymes & termes

Blade : moteur de templates Laravel. HTTP 500 : erreur serveur interne. Stack trace : détail technique parfois affiché si APP_DEBUG=true en production (fuite massive d’information).

Type d’attaque / scénario
  • Fuite d’information par erreur si le mode debug est actif hors développement (chemins, code, secrets en clair dans certaines configs).
  • Déni de service léger pour les utilisateurs visitant ces URLs (fonctionnalité cassée).

I3 Informative Routes dynamiques depuis la table modules À traiter si besoin

Les ressources admin sont créées à partir du slug en base (Module::where('is_active', true)). Une compromission de la base permettrait des changements de surface d’attaque ou un chargement non prévu si un contrôleur existe.

routes/admin.php
Acronymes & termes

ORM / Eloquent : accès base de données par objets (Laravel). Ici les slugs en base pilotent les segments d’URL.

Type d’attaque / scénario
  • Post‑compromise base de données : attaquant modifie modules.slug pour exposer de nouvelles routes (si un contrôleur existe) ou créer confusion.
  • Attaque hors ligne du problème primaire (SQL injection, fuites de backups, privilège DBA).

I4 Informative Middleware CheckAdminModulePermission À traiter si besoin

Les méthodes de contrôleur nommées autrement que les actions REST classiques créent une permission du type module.action littérale. Il faut s’assurer que les permissions correspondantes sont bien créées dans Spatie ; sinon soit tout le monde est bloqué, soit des contournements par rôle peuvent être possibles selon votre politique (super_admin passe par Gate::before). Revue métier conseillée.

app/Http/Middleware/CheckAdminModulePermission.php
Acronymes & termes

Gate : façade Laravel pour l’autorisation. Spatie Laravel Permission : paquet tiers pour rôles/permissions persistés en base. CRUD : opérations classiques mapping vers view, create, edit, delete.

Type d’attaque / scénario
  • Incohérence de droits : action sensible accessible à un rôle qui ne devrait pas (si la permission n’existe pas et que le défaut est permissif), ou inversement blocage opérationnel.
  • Super-admin bypass : compte super_admin contourne les permissions — à surveiller sur la gouvernance des comptes.

Points positifs remarqués

Prochaines étapes suggérées

  1. Corrigé le 04/05/2026 Open redirect H1 · AdminLocaleController::switch
  2. Corrigé le 06/05/2026 Validation harmonisée des clés sfer.php (M2) · SettingsController.
  3. Corrigé le 06/05/2026 Exposition de $e->getMessage() supprimée côté client pour SettingsController et TinyMCEController (L1) — report($e) côté serveur. Poursuivre la même règle sur les autres contrôleurs admin si besoin.
  4. Corrigé le 06/05/2026 Webhooks Stripe (L2) : signature + middleware IP déjà en place ; maintenir la liste d’IPs alignée sur Stripe.
  5. Planifier un durcissement progressif CSP (M1, nonce) par zone de l’application.
  6. Corriger ou ajouter la vue resources/views/pages/show.blade.php (I2) référencée par PageController::show.
  7. Compléter l’audit par des tests dynamiques (OWASP ZAP, Burp), revue dépendances (composer audit) et configuration serveur (.env hors production).