Disponible pour de nouveaux projets Accra, Ghana
Tous les écrits

Cybersécurité · Authentification · OAuth · Privilège minimum

Verrouiller la porte de derrière : comment j'ai sécurisé la connexion d'administration de mon site Web

Qui contrôle le panneau d'administration d'un site Web contrôle ce que chaque visiteur voit et exécute. Voici comment l'administrateur de ce site fonctionne sans serveur à pirater, pourquoi sa clé n'ouvre qu'une seule porte, et la faille de chaîne d'approvisionnement que je dois encore combler.

Par Festus Boamah · · 4 min read

La porte d'entrée d'un site Web est la partie que les visiteurs voient. Le panneau d'administration est la porte de derrière, et les attaquants le savent. Qui contrôle l'administration contrôle ce que chaque visiteur lit, et dans le pire des cas, quel code s'exécute dans leurs navigateurs. Une page d'accueil défigurée est embarrassante. Une page d'accueil qui sert discrètement un script malveillant à tous les visiteurs est bien pire.

Donc, lorsque j'ai ajouté un éditeur à ce site Web, pour que je puisse publier des articles depuis mon téléphone sans ouvrir un éditeur de code, j'ai traité sa connexion comme la partie la plus sensible de l'ensemble du système.

Diagramme du flux de connexion administrateur : le CMS dans le navigateur demande à un petit auth Worker de se connecter, le Worker envoie l'utilisateur à GitHub avec une valeur d'état aléatoire, GitHub retourne un code, et le Worker retourne un jeton uniquement à festusboamah.com. Chaque sauvegarde devient un commit Git vers main.

Décision 1 : pas de serveur à pirater

La plupart des systèmes de gestion de contenu s'exécutent sur un serveur avec une base de données d'utilisateurs et de mots de passe. Ce serveur devient une cible : un plugin obsolète, un mot de passe faible ou une base de données exposée peut donner à un attaquant l'ensemble du site.

Ce site n'a rien de tout cela. Il s'agit d'un site statique, reconstruit à partir d'un référentiel Git sur GitHub chaque fois que quelque chose change. Le panneau d'administration (Sveltia CMS) n'est qu'une application JavaScript qui s'exécute dans mon navigateur et communique directement avec l'API de GitHub. Enregistrer un article signifie effectuer un commit.

Cela déplace la question de sécurité. Il n'y a pas de serveur d'administration à patcher et pas de base de données de mots de passe à fuir. La vraie question devient : qui peut écrire dans le référentiel ? Tout le reste découle de la réponse.

Décision 2 : une clé qui ouvre une seule porte

Pour écrire dans le référentiel, le CMS a besoin d'un jeton de GitHub. La façon facile d'en obtenir est une application OAuth classique avec la repo portée. Mais ce jeton serait en mesure de lire et d'écrire chaque référentiel que mon compte peut atteindre, public et privé. Si cela fuyait, les dégâts iraient bien au-delà de ce site Web.

Au lieu de cela, la connexion se fait via une application GitHub qui est :

  • installée sur ce référentiel uniquement, et
  • dotée de une seule autorisation : Contenu (lire et écrire). Elle ne peut pas toucher les problèmes, les paramètres, les flux de travail, d'autres référentiels ou mon compte.

C'est le principe du privilège minimum : donner à chaque clé le pouvoir le plus petit qui fasse encore l'affaire, afin que lorsque (et non si) quelque chose se passe mal, la zone d'impact soit petite.

Décision 3 : protéger la remise

Le flux de connexion OAuth passe par un petit Cloudflare Worker (le sveltia-cms-auth open source, vendu dans mon référentiel non modifié à un commit épinglé, donc je sais exactement quel code s'exécute). Il détient le secret client de l'application GitHub en tant que secret Worker chiffré, jamais dans le code ou le référentiel. Trois de ses vérifications valent la peine d'être expliquées, car chacune bloque une attaque réelle :

  • Liste d'autorisation de domaine. Le Worker ne démarre une connexion que pour festusboamah.com. Quelqu'un qui copie ma page d'administration sur son propre domaine ne peut pas utiliser mon aide de connexion.
  • Protection CSRF avec state. Avant de m'envoyer sur GitHub, le Worker génère une valeur aléatoire, la stocke dans un HttpOnly, Secure cookie, et la transmet à GitHub en tant que paramètre OAuth state. Lorsque GitHub me renvoie, les deux doivent correspondre. Cela empêche un attaquant de tromper mon navigateur pour terminer une connexion que ils ont initiée.
  • Livraison avec vérification d'origine. Le jeton est remis à la fenêtre CMS avec postMessage. Le Worker ne le libère que lorsque l'origine signalée par le navigateur de la fenêtre demandante est mon domaine. Cette origine est définie par le navigateur, et non par la page, elle ne peut donc pas être falsifiée.

Décision 4 : rendre chaque modification responsable

Parce que chaque sauvegarde est un commit Git sur main, chaque modification du site a un auteur, une horodatage et une différence exacte. En termes de STRIDE, cela répond au problème de répudiation : rien ne change sur le site sans enregistrement. Cela me donne également une récupération instantanée : si quelque chose de mauvais était jamais publié, je peux annuler le commit et Cloudflare Pages reconstruit la version propre en environ une minute.

Les pages d'administration reçoivent également leurs propres en-têtes HTTP : une politique de sécurité du contenu séparée, X-Robots-Tag: noindex pour que les moteurs de recherche ne les listent pas, et Cache-Control: no-store pour que rien de sensible ne soit mis en cache.

Où cela est encore faible

  • Le script CMS n'a pas de vérification d'intégrité. La page d'administration charge Sveltia CMS à partir du CDN unpkg. Je fixe une version exacte, ce qui me protège contre les mises à niveau surprises, mais il n'y a pas de hachage d'intégrité des sous-ressources. Si le CDN était jamais compromis et servait un fichier modifié, mon navigateur l'exécuterait avec un jeton GitHub valide à portée. Il s'agit d'un risque classique de chaîne d'approvisionnement logiciel. La solution consiste à ajouter un integrity hachage sur la balise de script ou à servir le fichier à partir de mon propre domaine.
  • La CSP d'administration est plus lâche que celle du site. Pour charger le CMS, la politique d'administration autorise les scripts à partir d'unpkg et les scripts intégrés. Cela signifie que si un bug d'injection existait dans l'administration, la politique ne l'empêcherait pas d'exécuter un script provenant de n'importe quel package sur unpkg. L'hébergement autonome du CMS me permettrait de resserrer cela également.
  • Tout repose sur un seul compte GitHub. L'application limite ce qu'un jeton peut faire, mais celui qui contrôle mon compte GitHub contrôle le site. Cela rend l'authentification à deux facteurs solide sur ce compte (idéalement un passeport ou une clé de sécurité plutôt que SMS) le contrôle le plus important dans cette conception, même s'il ne se trouve pas dans le code.

La leçon

La décision de sécurité la plus utile ici n'était pas un contrôle ingénieux. C'était la suppression du serveur, puis la réduction de la seule clé restante à sa plus petite taille possible. Avant d'ajouter des défenses, il est toujours utile de se demander : qu'est-ce que je peux supprimer, afin qu'il y ait moins à défendre ?

Avez-vous repéré quelque chose que j'ai manqué ? Dites-le-moi.

Festus Boamah

Travaillons ensemble

Construisons quelque chose qui compte.

Un produit à construire, une marque à façonner ou un cours à donner ? J’aimerais beaucoup en entendre parler.