Base de travail V7.8 reprise du répertoire de travail
Commit technique : reproduit à l'identique l'état non commité de la copie de travail (sécurisation V7.8, CSP à nonce, Session, migration des droits) afin que le travail d'ergonomie qui suit soit isolable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
134
README_V7_8.md
Normal file
134
README_V7_8.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 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
|
||||
|
||||
1. Sauvegarder la base.
|
||||
2. Exécuter une seule fois :
|
||||
|
||||
```sql
|
||||
SOURCE database/migration_v7_8_securite_acces.sql;
|
||||
```
|
||||
|
||||
3. Reconstruire les images, phpMyAdmin compris :
|
||||
|
||||
```bash
|
||||
docker compose up -d --build php nginx
|
||||
docker compose up -d --force-recreate phpmyadmin
|
||||
```
|
||||
|
||||
## 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.
|
||||
Reference in New Issue
Block a user