6.6 KiB
PTA V7.8 — Correctifs de sécurité et descente des droits en base
Cette version ne change aucune règle métier. Elle corrige les failles relevées lors de l'audit et déplace l'intégralité des règles d'accès dans la base.
Failles corrigées
1. phpMyAdmin ouvert sans mot de passe (critique)
docker-compose.yml fournissait PMA_USER et PMA_PASSWORD au conteneur
phpMyAdmin. Ces deux variables font basculer phpMyAdmin en auth_type=config :
toute personne atteignant le port arrivait sur une session déjà
authentifiée, avec accès complet en lecture et en écriture à la base. Le
.env publiait de surcroît ce port sur 0.0.0.0, donc sur tout le réseau.
Les deux variables ont été retirées et le port est revenu sur 127.0.0.1.
phpMyAdmin réclame désormais un identifiant MariaDB à chaque connexion. Pour y
accéder depuis un poste distant : ssh -L 8081:127.0.0.1:8081 utilisateur@serveur.
2. XSS stocké via les attributs HTML
PTA.utils.escapeHtml s'appuyait sur textContent / innerHTML, qui
n'échappent ni " ni '. La fonction était pourtant utilisée dans une
vingtaine de contextes d'attribut (value="…", title="…", aria-label="…").
Un libellé contenant un guillemet permettait de sortir de l'attribut et
d'injecter du script.
Les vecteurs réalistes étaient les libellés de vacances scolaires — importés depuis une API externe — ainsi que les noms de lieux, les noms d'agents, les libellés de diplôme et les commentaires PTA.
escapeHtml échappe désormais les cinq caractères et reste valable en contexte
texte comme en contexte attribut. L'implémentation dupliquée escapeAttr de
annual-overview.js a été supprimée au profit de la fonction commune.
3. Règles de refus nginx sans effet
Les blocs interdisant config.php, db.php et page_bootstrap.php étaient
placés après location ~ \.php$. nginx retenant la première expression
régulière correspondante, ces refus n'ont jamais été appliqués : les trois
fichiers d'amorçage étaient exécutables depuis le navigateur.
L'ordre des blocs a été corrigé et la couverture élargie (/docker/, fichiers
.env, .sql, .ini, .log, .md).
4. Divulgation d'informations techniques
PTA_DEBUG valait 1 par défaut : le message des exceptions — requêtes SQL,
noms de tables, chemins serveur — était renvoyé au navigateur. Les pages
d'erreur de page_bootstrap.php et de role.php affichaient de la même façon
getMessage(), et db.php y ajoutait l'hôte, l'utilisateur et le nom de la
base issus du message PDO.
Le mode debug est désormais désactivé par défaut. Tout détail technique est
journalisé côté serveur via App\Core\Security::reportThrowable() et n'atteint
le client que si PTA_DEBUG est explicitement activé.
5. Absence d'en-têtes de sécurité
Les réponses HTML portent maintenant une Content-Security-Policy à nonce :
script-src 'self' 'nonce-…', sans unsafe-inline. PTA ne contient aucun
gestionnaire d'événement inline ni style inline, la politique peut donc rester
stricte. S'y ajoutent X-Content-Type-Options, X-Frame-Options,
Referrer-Policy, Permissions-Policy, Cross-Origin-Opener-Policy et, en
HTTPS, Strict-Transport-Security.
Tout nouveau <script> inline doit porter nonce="<?= $cspNonce ?>", sinon le
navigateur refusera de l'exécuter.
6. Session et CSRF
session_start() était appelé à trois endroits sans paramètres de cookie.
App\Core\Session::start() centralise le démarrage et impose HttpOnly,
SameSite=Lax, use_strict_mode et secure en HTTPS — y compris hors du
conteneur Docker, où le php.ini fourni ne s'applique pas.
Le changement de rôle (role.php?action=reset), qui modifiait l'état de la
session via un simple GET, exige désormais le jeton CSRF. Le jeton est
régénéré à chaque changement de rôle.
7. Secrets versionnables
Le dépôt n'avait ni .gitignore ni .dockerignore. Le .env contenant les
mots de passe MariaDB était prêt à être commité, et COPY . /var/www/html
copiait .env et .git dans l'image publiée. Les deux fichiers ont été créés.
Descente des droits en base
app/Core/Access.php ne contient plus aucune règle : la matrice des droits et
les périmètres de visibilité sont lus dans des vues SQL.
| Vue | Rôle |
|---|---|
v_acces_role_page |
pages autorisées par rôle |
v_acces_role_permission |
permissions fonctionnelles par rôle |
v_acces_identite_agent |
identités d'agents actifs sélectionnables |
v_acces_identite_structure |
lieux d'affectation actifs sélectionnables |
v_acces_agent_par_structure |
quels agents un lieu peut consulter (rattachement ou planif.) |
v_acces_perimetre_creneau |
lieu et planning d'un créneau, avant modification |
v_acces_perimetre_planning |
lieu et agent d'un planning, avant validation |
Les deux premières s'appuient sur les tables acces_role_page et
acces_role_permission : modifier un droit se fait désormais en SQL, sans
redéploiement. Une permission absente de la base vaut « refusé ».
SchemaModel::assertCurrent() vérifie la présence des sept vues au démarrage.
Si la migration n'a pas été jouée, l'application refuse de démarrer plutôt que
de tourner sans contrôle de périmètre.
Installation
La migration est appliquée automatiquement au déploiement. Le service jetable db-migrate
du compose joue database/migration_v7_8_securite_acces.sql dès que MariaDB est sain, et le
service php n'est démarré qu'après sa sortie en succès.
- Sauvegarder la base.
- Déployer :
docker compose up -d --build
docker compose logs db-migrate # doit afficher « Base à jour »
La migration est idempotente et suivie dans la table schema_migration : un second déploiement
l'ignore. Elle reste jouable à la main si besoin :
SOURCE database/migration_v7_8_securite_acces.sql;
Reste à traiter
Le sélecteur de role.php n'est toujours pas une authentification :
n'importe quel visiteur peut choisir « Service Enfance » et obtenir le contrôle
complet de l'application. Tant que ce point n'est pas traité, aucune des
corrections ci-dessus ne protège une instance exposée sur Internet.
Le mot de passe MariaDB doit également être changé : le .env reprend encore la
valeur d'exemple.