Headers HTTP pour la sécurité avec Drupal

Créé le 18/05/2020

Dernière mise à jour le 07/06/2020

La semaine dernière, j'ai vu passer dans mon fil d'actualités Twitter, ce tweet https://twitter.com/Stargayte/status/1260654141421961216 concernant le site https://observatory.mozilla.org qui permet entre autres de scanner les sites pour voir le paramétrage des en-têtes HTTP relatifs à la sécurité.

J'ai bien évidemment scanné mon site, rectifier certains paramétrages, modifier mes outils pour que cela y soit d'office et voilà un sujet d'article à écrire !

Constat

Voici le résultat initial, score de 40/100 (aïe !), les tests ne passant pas étaient :

  • Content Security Policy
  • HTTP Strict Transport Security
  • X-Content-Type-Options
  • X-XSS-Protection

Paramétrage :

  • HTTPS et redirection HTTP -> HTTPS déjà présente

HTTP Strict Transport Security et X-XSS-Protection

Ajouté dans la configuration du serveur web, Apache dans mon cas :

<IfModule mod_headers.c>
Header set X-XSS-Protection "1; mode=block"
Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"
</IfModule>

X-Content-Type-Options

En réalité ce header est déjà géré par Drupal, mais le problème c'est qu'il est présent 2 fois et avec la même valeur.

Il y a un « event subscriber » dans le noyau qui ajoute le header et le header est ajouté une seconde fois avec la configuration Apache fournie par Drupal.

J'ai ouvert une issue pour signaler le problème, car chercher « X-Content-Type-Options » dans les issues n'a rien donné et en fait une autre issue existe déjà depuis plusieurs années.

Si on veut résoudre le problème en attendant le consensus, ce n'est pas compliqué, il suffit de retirer le header et de le remettre par un des composants de la chaîne d'appel (Apache, Varnish ou Nginx). Après comme le header est déjà présent, je ne suis pas pressé, je vais attendre que l'issue soit résolue.

Content Security Policy

Allons y directement, le point le plus compliqué (pour rester poli) à traiter et à faire une règle la plus générique possible.

Règle qui sera sans doute à adapter finement par site.

Le résultat obtenu est le suivant, c'est le résultat de l'application des recommandations du site de Mozilla, tout en permettant à Drupal de fonctionner :

Content-Security-Policy: "base-uri https 'self'; connect-src https 'self'; default-src 'none'; font-src https: 'self'; frame-ancestors https 'self'; img-src https: data:; object-src 'none'; script-src https: 'unsafe-inline'; style-src https: 'unsafe-inline'"

Edit 23/05/2020: Attention : La configuration précédente est érronée, il faut https: et non https. Voir la partie "Edit 23/05/2020".

Et voici le détail de chaque règle :

  • base-uri : pour éviter de charger des JS externes.
  • connect-src : pour permettre à certains modules de continuer à fonctionner :
    • Contextual Links,
    • Quich Edit.
  • default-src : ‘none' pour éviter des comportements non prévus.
  • form-action : non implémenté car sinon j'avais un comportement étrange, les formulaires étaient soumis (car par exemple de la configuration était enregistrée) mais la page ne se rechargeait pas et il y avait un message d'erreur dans la console navigateur disant que le formulaire n'avait pas été soumis. Je ne sais pas si cela provenait de mon environnement de développement avec Traefik en reverse-proxy.
  • Frame-ancestors : pour contrôler quel site peut ouvrir une iframe ou un embed sur le site.
  • Img-src : data pour permettre à Blazy de fournir une image le temps que le lazy-loading charge la vraie image.
  • Object-src : none pour bloquer ce genre de balise.
  • Script-src : unsafe-inline pour autoriser certains JS à fonctionner :
    • CKEditor,
    • Matomo.
  • style-src : unsafe-inline sinon certains styles sont retirés :
    • styles générés par le JS de Modernizr,
    • style en ligne de CKEditor.

Bonus : un peu d'offuscation

L'offuscation n'est pas du tout ma philosophie, mais bon, si ça permet d'éviter que ça ne déclenche l'attaque de robots passant par là du fait d'exposer des headers en trop, autant la mettre en place...

Je l'avais déjà mise en place à la mise en ligne du site, mais là j'ai décidé de la mettre par défaut dans mes outils.

  • PHP :
    • expose_php = off
  • Apache :
    • ServerSignature Off
  • Varnish :

    • sub vcl_deliver {
      …
      unset resp.http.cache-tags;
      …
      }
      
  • Nginx (équivalent Traefik) :

    • more_clear_headers 'Server';
      more_clear_headers 'Via';
      server_tokens off;

Conclusion

Si on compte X-Content-Type-Options comme résolu, cela va être compliqué d'avoir un meilleur score que 80/100 à cause du header Content Security Policy.

Un moyen d'aller plus loin serait d'avoir un nom de domaine dédié (ou un serveur dédié) pour les accès administrateur (, contributeur ou modérateur) car Modernizr ne concerne que l'administrateur et en règle général CKEditor également. Donc hormis Matomo, il serait possible de ne pas avoir à mettre unsafe-inline sur style-src et script-src.

Pour Matomo, je pense qu'en revoyant le fonctionnement du module et en lui faisant charger un fichier JS généré comme le permet de le faire le module GoogleTagManager, cela devrait résoudre le problème.

Après il faut voir sur des sites plus complexes, à partir du moment où il va y avoir besoin d'une intégration avec un service externe qui fonctionne avec du JS inline, cela risque de devenir très coûteux pour ne pas dire impossible selon les contraintes projets (planning, coût).

Edit 23/05/2020

Suite à la prise de connaissance du module Content-Security-Policy, j'ai décidé de retirer l'ajout de ce header par Nginx (ou Traefik) et d'utiliser le module Drupal à la place.

En effet, le module va permettre une gestion fine du header, chose que ne permettra pas (ou difficilement) une configuration serveur. De plus, vu qu'il faut qu'il soit adapté aux besoins du site, autant que ce soit l'applicatif qui gère le header.

Un avantage du module, il m'a permis de me rendre compte que dans ma configuration précédente, j'avais fait des erreurs de saisies 'https', il faut saisir 'https:'. Point que le module vérifie à l'enregistrement de la configuration.

La correction du https: a mis en lumière que ce n'était pas une configuration considérée comme sécurisée car trop permissive pour base-uri, frame-ancestors et script-src. Mais c'est ok pour img-src. Il faut vraiment une liste blanche des domaines à autoriser. J'ai laissé https: pour img-src car je m'en sers par exemple pour le copyleft en bas de page et des images sur ma page Badges.

Finalement, j'ai fait des patchs pour les modules Blazy et Matomo, chose que j'avais mis dans un "Pour aller plus loin" dans la conclusion d'origine, mais que j'ai été obligé de faire pour utiliser sereinement le module CSP. L'issue regroupant les liens vers les modules concernés : https://www.drupal.org/project/csp/issues/3099548

Pour information, voici la configuration du module CSP que j'ai au moment de l'écriture :

 report-only:
  enable: false
  directives:
    script-src:
      base: self
    script-src-attr:
      base: self
    script-src-elem:
      base: self
    style-src:
      base: self
    style-src-attr:
      base: self
    style-src-elem:
      base: self
    frame-ancestors:
      base: self
  reporting:
    plugin: none
enforce:
  enable: true
  directives:
    default-src:
      base: none
    connect-src:
      base: self
    font-src:
      base: self
    img-src:
      sources:
        - 'https:'
      base: self
    object-src:
      base: none
    script-src:
      base: self
    style-src:
      base: self
    base-uri:
      base: self
    form-action:
      base: self
    frame-ancestors:
      base: self
  reporting:
    plugin: none

Cette configuration ne sera pas forcément mise à jour dans l'article, et n'est là que pour servir d'exemple à adapter à vos besoins. Du fait de la complexité de fournir une valeur par défaut, je ne vais pas non plus scripter une telle valeur dans mon template de projet Drupal.

J'ai également testé le module Security Kit qui va au-delà du header Content-Security-Policy, mais bien que plus utilisé, il offre moins d'options que le module Content-Security-Policy concernant le header qui nous intéresse et il n'offre pas d'événement permettant d'adapter le header automatiquement dans des modules communautaires ou spécifiques.

Edit 07/06/2020

Afin de permettre d'avoir des cookies de session en "secure" sur mon environnement de développement, j'ai géré les settings reverse_proxy et reverse_proxy_addresses dans https://gitlab.com/florenttorregrosa-drupal/docker-drupal-project.

J'ai voulu re-tester le "form-action" qui posait problème auparavant, c'est désormais OK. Visiblement même si en local, Drupal générait des liens en HTTPS, il ne considérait pas les requêtes comme "secure" et donc agissait comme s'il était en HTTP.

Remerciements

Merci à Philippe Joulot (phjou) pour ses tweets sur https://observatory.mozilla.org et sur le module Content-Security-Policy.

Merci à Geoff Appleby (gapple), mainteneur du module Content-Security-Policy, pour son aide.

Liens

Ressources :

Commentaires

Soumis par Stéphane (non vérifié) le mer, 09/30/2020 - 07:33 - Permalien

dans la partie mod_header, nous avons l'habitude d'ajouter également
Header unset X-Powered-By
Header unset Server
Header unset ETag
Header always append X-Frame-Options SAMEORIGIN
Header set X-Content-Type-Options: "nosniff"

et si l'applicatif n'est pas configuré correctement pour les cookies secure

Header always edit Set-Cookie ^(.*)$ $1;SameSite=Strict;HttpOnly,Secure

Merci pour le commentaire.

X-Powered-By : je ne l'ai pas mentionné dans l'article, mais dans la vidéo de la présentation faite à un meetup https://www.youtube.com/watch?v=EjsQzpiHjz8, je dis que ça ne me dérange pas de le laisser. Mais il est vrai qu'on peut l'enlever.

Server : normalement avec les directives de l'article, il ne devrait plus être présent.

ETag : Quel est l'intérêt de retirer ce header ? Une rapide recherche m'indique que c'est pour éviter que des internautes ne puissent être pistés. Mais ne perd-t-on pas son utilisé au niveau gestion du cache ?

X-Frame-Options et X-Content-Type-Options sont déjà mis via le noyau Drupal dans l'event subscriber FinishResponseSubscriber.

Ok pour cookie secure, je ne connaissais pas.

Je comprends que l'on préfère gérer tous ces headers côté serveur.

Ajouter un commentaire