# ADR-016 : Les en-têtes de sécurité sont portés par l'application

**Statut :** Accepté
**Date :** 2026-10-17
**Contexte :** Écart découvert entre staging et production en préparant la mise à jour de la page d'accueil (MR !51). Prolonge le correctif CSP de l'audit WES du 2026-07-20 (critique n°2) et l'ADR-001 (JWT en localStorage).

---

## Contexte

`App\Http\Middleware\SecurityHeaders` envoie un CSP strict, avec un nonce par requête et sans `'unsafe-inline'`. C'est lui qui empêche un script injecté de lire le JWT stocké en `localStorage` (ADR-001).

En production, ce CSP n'arrivait jamais au navigateur. Sur server1, le fichier Apache `/etc/apache2/conf.d/userdata/ssl/2_4/welely/global_hardening.conf` applique `Header always set` à tous les sites du compte `welely`. Son CSP est permissif (`'unsafe-inline'`, `https:` dans `script-src` et `connect-src`) et il fixe aussi `X-Frame-Options: SAMEORIGIN`. Comme PHP tourne en SAPI CGI, l'en-tête envoyé par Laravel est rangé dans la même table qu'Apache : il est écrasé. Un `Header always unset` propre au domaine le supprime également ; l'essai du 17/10/2026 a laissé la production sans CSP pendant environ 3 minutes avant le retour arrière.

La staging passe par un vhost dédié (`/etc/apache2/conf.d/staging-gestionstock.conf`) qui n'inclut pas ce fichier : elle recevait le CSP de l'application. Les deux environnements se comportaient donc différemment. Les gestionnaires `onclick` en ligne de la page d'accueil fonctionnaient en production et étaient bloqués en local et en staging.

## Décision

1. **L'application est la seule source de ses en-têtes de sécurité** : CSP, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, `Permissions-Policy` et HSTS. Leur valeur est versionnée, revue et testée (`tests/Feature/SecurityHeadersTest.php`), identique dans tous les environnements.
2. **Côté serveur**, les lignes CSP et `X-Frame-Options` de `global_hardening.conf` portent la condition `"expr=!(%{SERVER_NAME} in {'gestionstock.welely.com', 'www.gestionstock.welely.com'})"`. Les autres sites Welely gardent exactement leurs en-têtes. Les en-têtes globaux restants (HSTS, nosniff, Referrer-Policy, Permissions-Policy) ont la même valeur que ceux de l'application : ils servent de filet pour les fichiers statiques, qui ne passent pas par Laravel.
3. **`Permissions-Policy`** est ajoutée au middleware avec la valeur déjà appliquée par Apache (`geolocation=(), microphone=(), camera=()`), pour que la staging reçoive la même que la production. L'application n'utilise aucune de ces API. Une fonction future qui en aurait besoin (par exemple un scan de code-barres à la caméra) devra modifier **à la fois** le middleware et le fichier Apache, tant que ce dernier les fixe.
4. **Aucun gestionnaire d'événements en ligne** (`onclick=`, `onchange=`…) dans les vues Blade : les écouteurs sont posés dans un `<script nonce>`. Un test sur la page d'accueil le vérifie (`LandingPlanFeaturesTest`).

## Alternatives écartées

- **Laisser Apache décider du CSP** : la valeur n'est ni versionnée ni testée, elle diffère selon l'environnement, et elle neutralise la protection du JWT.
- **`Header always unset` dans un include propre au domaine** : inopérant avec PHP en CGI, il supprime aussi l'en-tête de l'application (essai du 17/10/2026).
- **`Header setifempty` dans le fichier global** : changerait le comportement de tous les sites Welely, dont certains dépendent peut-être de la politique permissive.
- **Vhost dédié en production, comme la staging** : fonctionne, mais échappe à la gestion cPanel (version de PHP, réglages du domaine).

## Conséquences

- La production applique enfin le CSP strict : un script injecté ne s'exécute pas et ne peut pas envoyer de données vers un autre domaine.
- Le fichier Apache partagé contient une exception nommée. Une nouvelle application Welely qui envoie son propre CSP devra y être ajoutée. Ce point doit figurer dans la procédure de mise en service d'un site sur server1.
- Les en-têtes de production se vérifient avec `curl -I` sur l'URL publique, pas seulement dans les tests : les tests ne voient pas la configuration Apache.
- Retour arrière : restaurer `global_hardening.conf.bak.20261017_200956`, puis faire un redémarrage gracieux d'Apache.
