← Portfolio Audit Sécurité · psyker.fr Me contacter
Preuve documentée · Boucle de Refus · Sécurité PHP

7 refus avant déploiement —
Audit sécurité psyker.fr

Avant mise en ligne de psyker.fr, chaque livrable de code a été soumis à un audit de sécurité avec les mêmes critères d'acceptance qu'imposés à un prestataire externe. Sur 7 livrables audités, 7 ont été refusés à la première livraison. Aucun refus contourné. Résultat : score A+ securityheaders.com.

Yohan Fauck
Yohan Fauck Chef de Projet · Product Owner
psyker.fr · PHP pur · Hostinger mutualisé
Stack : PHPMailer · Zéro framework
7 / 7 Livrables refusés à la 1ère livraison
0 Refus contournés ou ignorés
A+ Score securityheaders.com en prod
0 Incident sécurité depuis la mise en ligne
Contexte

Le projet · la posture adoptée

psyker.fr est une plateforme B2C/B2B de rédaction sur mesure développée de zéro en 45 jours sans prestataire externe. Sur ce projet, j'étais à la fois PO, développeur par orchestration IA et responsable de la mise en production.

La posture retenue était simple : même rigueur qu'avec un prestataire externe payé au livrable. Chaque livraison de code était soumise à un audit avant d'être acceptée. La pression de livrer vite sur un projet solo en 45 jours est réelle — la Boucle de Refus est le seul dispositif qui résiste structurellement à cette pression, parce qu'elle est documentée avant de coder, pas négociée après.

Principe de validation retenu

"Refuser de déployer vaut mieux que corriger une faille en production."


Synthèse

Les 7 refus — vue d'ensemble

Criticité différenciée : CRITIQUE = bloque le déploiement, risque d'exploitation immédiate. ÉLEVÉE = surface d'attaque directe. FONCTIONNELLE = bug masqué, fiabilité compromise.

# Livrable refusé Risque concret Criticité
#1 Credentials SMTP en dur dans config.php Exposition mot de passe si dépôt partagé ou snapshot copié Critique
#2 Rate limiting via $_SESSION['last_send'] Session côté client — bypassable en vidant le cookie. Protection réelle : zéro Critique
#3 Inputs formulaire sans whitelist vers PHPMailer Surface d'injection sur chaque champ non validé Élevée
#4 Stockage rate limit dans /tmp/ partagé Sur mutualisé : /tmp/ lisible par tous les vhosts du serveur Élevée
#5 Accès HTTP direct aux dossiers /includes/, /config/, /articles/ Exposition de la logique interne — fichiers PHP exécutables en direct Élevée
#6 writeLikes() retourne HTTP 200 en cas d'échec disque État affiché côté client non confirmé côté serveur — bug de fiabilité masqué Fonctionnelle
#7 htmlspecialchars() sur champ email dans mail admin Entités HTML visibles en clair dans la boîte mail de l'admin Fonctionnelle

Détail

Refus documentés — ce qui a été exigé

Pour chaque refus : le livrable soumis, la justification du rejet par le risque concret, la correction imposée avant déploiement.

Refus #1 — Credentials SMTP en dur dans config.php
Critique

Livrable soumis : config.php avec identifiants SMTP et mot de passe en clair dans le code source.

Justification du refus : Un fichier avec mot de passe en clair est un risque critique d'exposition immédiate si le dépôt est partagé, si un snapshot est copié, ou si le fichier est mal protégé côté hébergeur. La surface d'exposition est incontrôlable une fois le fichier dans un système de versioning ou sur un serveur mal configuré.

Correction exigée avant déploiement

Variables d'environnement exclusivement. config/secrets.php avec putenv() + .htaccess 'Require all denied'. Fail-fast si variable absente : HTTP 503, jamais de fallback hardcodé.

Refus #2 — Rate limiting via $_SESSION['last_send']
Critique

Livrable soumis : Limitation du nombre d'envois de formulaire via une variable de session PHP.

Justification du refus : Une session PHP est côté client. Un attaquant vide son cookie de session et contourne la limite sans effort ni compétence technique. Protection réelle contre le spam ou le brute force : zéro. Ce n'est pas une défense — c'est une illusion de défense.

Correction exigée avant déploiement

Rate limiting fichier-serveur dans /data/ratelimit/ (inaccessible web). IP hashée SHA-256 + sel RGPD, fenêtre glissante 10 min, 3 envois max. Nettoyage automatique 1% des requêtes.

Refus #3 — Inputs formulaire passés directement à PHPMailer sans whitelist
Élevée

Livrable soumis : Champs 'sujet', 'délai', 'budget' transmis à PHPMailer sans validation de valeur.

Justification du refus : Sans whitelist stricte, chaque champ accepte n'importe quelle valeur. Surface d'injection inutile : chaque champ inattendu passé en base de données ou en email est une faille potentielle. La règle est simple — tout ce qui n'est pas explicitement autorisé est interdit.

Correction exigée avant déploiement

Whitelist exhaustive sur chaque champ énuméré. Toute valeur hors liste = rejet immédiat HTTP 400. Validation séparée email, longueurs min/max, anti-header injection sur le champ nom (suppression \r\n\t).

Refus #4 — Stockage rate limit dans /tmp/ partagé
Élevée

Livrable soumis : Fichiers de rate limit écrits dans /tmp/ du serveur.

Justification du refus : Sur hébergement mutualisé, /tmp/ est partagé entre tous les vhosts de la machine. Un autre site hébergé sur le même serveur peut lire ou polluer les fichiers de rate limit, contournant la protection ou générant des faux positifs qui bloquent des utilisateurs légitimes. Incompatible avec un environnement mutualisé.

Correction exigée avant déploiement

Déplacement vers /data/ratelimit/ (sous document root, protégé .htaccess 'Require all denied'). dirname(__DIR__) pour le chemin absolu — robuste aux symlinks et vhosts non standards.

Refus #5 — Accès HTTP direct aux dossiers /includes/, /articles/, /config/
Élevée

Livrable soumis : Architecture sans blocage des dossiers internes côté HTTP.

Justification du refus : Sans blocage explicite, un navigateur peut accéder directement à includes/config.php, articles/ecrit-001.php, etc. Sur Hostinger, les fichiers PHP s'exécutent à l'accès direct — risque d'exposition de la logique interne ou d'exécution non souhaitée. Défense en profondeur obligatoire sur chaque dossier sensible.

Correction exigée avant déploiement

.htaccess racine avec RewriteCond bloquant THE_REQUEST pour /includes/, /articles/, /config/, /data/, /errors/. .htaccess locaux 'Require all denied' sur /config/ et /data/ (deux couches de protection indépendantes).

Refus #6 — writeLikes() retourne HTTP 200 même en cas d'échec disque
Fonctionnelle

Livrable soumis : Fonction d'écriture des likes retournant toujours un succès HTTP.

Justification du refus : Une écriture échouée silencieuse produit un compteur incohérent non détectable côté admin. L'interface affiche un état que le serveur n'a pas confirmé. Un bug masqué par une réponse fausse est pire qu'un bug visible — il accumule de la dette fonctionnelle non traçable.

Correction exigée avant déploiement

writeLikes() retourne bool. En cas d'échec : HTTP 500, message d'erreur explicite, error_log avec id article + timestamp. Jamais de succès fictif — l'état renvoyé au client reflète toujours l'état réel du serveur.

Refus #7 — htmlspecialchars() sur le champ email dans le corps du mail admin
Fonctionnelle

Livrable soumis : Fonction sanitize() utilisant htmlspecialchars() sur tous les champs, y compris l'email.

Justification du refus : htmlspecialchars() encode les caractères HTML — pertinent pour l'affichage web, pas pour un email texte brut. Dans un email admin, cela produit des entités HTML visibles en clair (& au lieu de &). Bug fonctionnel direct, détectable immédiatement à la première commande client.

Correction exigée avant déploiement

sanitize() utilise strip_tags() + trim() uniquement. sanitizeEmailField() ajoute suppression \r\n\t (anti-header injection). Fonctions séparées selon le contexte de sortie — principe de séparation des responsabilités appliqué à la sanitization.


Bilan

Ce que ces 7 refus démontrent — compétences

La compétence rare ici n'est pas d'avoir identifié ces failles — un développeur senior les voit. Elle est d'avoir refusé de déployer sans correction, en maintenant la même rigueur qu'avec un prestataire externe payé au livrable. La Boucle de Refus n'est pas réservée aux livrables copy ou UX : elle s'applique à tout livrable soumis à des critères d'acceptance objectifs.

Délégation aveugle

L'IA livre du code. Le PO l'accepte parce que "ça marche". Les credentials sont en dur, le rate limit est bypassable, /tmp/ est partagé. En local, tout fonctionne. En production, la première tentative d'intrusion réussit.

Ownership technique

Chaque livrable audité contre des critères explicites avant validation. Les 7 refus bloquent le déploiement. Résultat : A+ securityheaders.com, résistance prouvée aux tentatives de brute force depuis la mise en ligne.

Ownership technique end-to-end

Lecture de chaque fichier critique avant validation. Pas de délégation sans recette — même quand l'exécutant est un modèle IA.

Refus documenté par le risque concret

Chaque rejet justifié par le risque mesurable, pas par une préférence. La criticité est différenciée : CRITIQUE, ÉLEVÉE, FONCTIONNELLE.

Secure by Design — défense en profondeur

Variables d'env, fail-fast, isolation des dossiers, RGPD by design sur les IPs hashées. Sécurité intégrée au cadrage, pas en correctif.

Rigueur de recette fonctionnelle

Refus #7 (htmlspecialchars) = anomalie fonctionnelle visible en prod — détectée avant déploiement. La recette couvre le comportement utilisateur, pas seulement la sécurité réseau.

Résultat en production
Ce que ce document démontre

Une posture PO —
pas une liste de bonnes pratiques

Ces 7 refus documentés représentent 7 décisions de ne pas déployer. Sur un projet solo avec deadline de 45 jours, chacune de ces décisions était un choix entre vitesse et rigueur. La rigueur a gagné à chaque fois — parce qu'elle était formalisée avant de commencer, pas négociée sous pression.

Si vous cherchez un Chef de Projet ou Product Owner capable d'imposer ce niveau d'exigence à une équipe ou à un processus d'intégration IA — disponible CDI, Toulouse et remote.