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

140 lines
6.6 KiB
Markdown

# 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 :
```bash
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 :
```sql
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.