Files
pta/docs/README_V7_8.md
2026-08-20 13:22:49 +02:00

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.

  1. Sauvegarder la base.
  2. 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.