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.
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.
"Refuser de déployer vaut mieux que corriger une faille en production."
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 |
Pour chaque refus : le livrable soumis, la justification du rejet par le risque concret, la correction imposée avant déploiement.
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é.
Variables d'environnement exclusivement. config/secrets.php avec putenv() + .htaccess 'Require all denied'. Fail-fast si variable absente : HTTP 503, jamais de fallback hardcodé.
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.
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.
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.
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).
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é.
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.
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.
.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).
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.
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.
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.
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.
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.
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.
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.
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.
Chaque rejet justifié par le risque mesurable, pas par une préférence. La criticité est différenciée : CRITIQUE, ÉLEVÉE, FONCTIONNELLE.
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.
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.
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.