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

Cybersécurité · Modélisation des menaces · Backend · Cloudflare Workers

Modélisation de menace d'un formulaire de contact : défense en profondeur sur une très petite API

Un formulaire de contact ressemble à la fonctionnalité la plus ennuyeuse sur un site Web. C'est également un point de terminaison public qui accepte du texte de la part d'inconnus et l'envoie dans une base de données et une boîte de réception. Voici comment j'ai modélisé les menaces de celui sur ce site, couche par couche, et où il est encore faible.

Par Festus Boamah · · 6 min de lecture

Le formulaire de contact sur ce site Web fait environ quarante lignes de formulaire et une route API. Il est facile de le traiter comme un détail. Mais regardez-le de l'autre côté : c'est un point de terminaison public, ouvert à tout l'internet, qui prend du texte de la part d'inconnus et l'écrit dans une base de données et une boîte de réception électronique. C'est exactement le type de fonctionnalité que les attaquants recherchent, car personne ne le surveille.

Avant de le publier, je me suis assis et j'ai modélisé les menaces. Cet article présente le processus : ce qui pourrait mal se passer, ce que j'ai construit contre chaque menace, et, tout aussi important, où la conception est encore faible.

Schéma du flux de données du formulaire de contact : le navigateur du visiteur envoie du JSON à travers une limite de confiance à un Cloudflare Worker, qui applique six couches (validation, piège à abeilles, limite de débit, Turnstile, SQL paramétré, e-mail sécurisé) avant de stocker le message dans D1 et d'envoyer un e-mail de notification

Commencez par le flux de données

La modélisation de menace commence par dessiner comment les données se déplacent. La mienne est courte :

  1. Un visiteur remplit le formulaire dans son navigateur.
  2. Le navigateur envoie du JSON à POST /api/contact, une petite application Hono exécutée sur Cloudflare Workers.
  3. Le Worker valide les entrées, vérifie les abus et stocke le message dans une base de données D1 (SQLite).
  4. Le Worker m'envoie une notification par e-mail, avec l'adresse du visiteur en tant que Reply-To.

Chaque flèche de cette liste est une limite de confiance : navigateur vers API, API vers base de données, API vers e-mail. Chacune est un endroit où des entrées non fiables traversent quelque chose que je tiens à cœur. Pour chaque limite, j'ai posé les questions derrière le modèle STRIDE : pourrait-on usurper quelque chose, altérer des données, nier ce que l'on a fait, divulguer des informations, nier un service ou élever ses privilèges ?

Pour un formulaire de contact, trois menaces émergent : abus (spam et inondation), injection (dans la base de données et dans l'e-mail), et vie privée (je détiens maintenant les noms, les adresses e-mail et les mots des autres).

Couche 1 : supposer que le navigateur ment

La première règle est que le formulaire dans le navigateur est une suggestion, et non un contrôle. N'importe qui peut contourner le formulaire et envoyer des requêtes avec curl. Donc, chaque vérification se produit à nouveau sur le serveur :

  • Le corps doit être un JSON valide, ou la requête est rejetée avec un 400.
  • name doit être présent et avoir au maximum 100 caractères.
  • email doit correspondre à un motif de base et avoir au maximum 200 caractères.
  • intent doit être l'un d'une liste fixe (project, design, teaching, speaking, other). C'est une liste d'autorisation : je n'essaie pas de deviner à quoi ressemble une entrée incorrecte, j'accepte uniquement ce qui ressemble à une entrée correcte.
  • message doit être compris entre 10 et 5 000 caractères.

Les limites de longueur ne sont pas seulement une question de propreté. Elles fixent un plafond à la quantité de stockage et d'e-mail qu'une demande peut consommer, ce qui compte plus tard lorsque nous parlons d'inondation.

La même idée s'applique à CORS. L'API ne renvoie les en-têtes CORS qu'aux origines sur une liste d'autorisation (mon domaine et ses déploiements de prévisualisation). Mais je ne compte pas CORS comme un contrôle de sécurité ici. Il empêche les autres sites Web d'utiliser le navigateur d'un visiteur pour appeler mon API ; il ne fait rien contre un script qui parle directement à l'API. Savoir quel contrôle protège contre quel attaquant est la moitié du travail.

Couche 2 : rendre le spam coûteux

Le spam est l'attaque la plus probable contre n'importe quel formulaire de contact, il reçoit donc trois couches séparées. Aucune d'entre elles n'est suffisante seule ; ensemble, elles augmentent le coût.

Un piège à miel. Le formulaire a un champ caché company que les gens ne voient jamais et ne remplissent jamais. Les bots simples remplissent chaque champ. Si company a une valeur, l'API renvoie { ok: true } et jette discrètement le message. Le retour du succès est délibéré : une erreur enseignerait à l'auteur du bot qu'il a été pris.

Une limite de taux. Chaque adresse IP reçoit cinq soumissions par heure, comptabilisées dans Cloudflare KV avec une expiration d'une heure. Au-delà de cela, l'API renvoie 429 Too Many Requests. Le décompte est effectué avant la vérification du bot, donc un script qui continue à échouer brûle toujours sa propre quote-part.

Un défi. Le formulaire utilise Cloudflare Turnstile, et le serveur vérifie le jeton avec Cloudflare avant de stocker quoi que ce soit. La clé secrète vit dans un secret Worker, jamais dans le référentiel ou la page.

Ceci est la défense en profondeur à petite échelle : le piège à miel attrape les bots paresseux gratuitement, Turnstile attrape la plupart des autres, et la limite de taux limite les dégâts causés par tout ce qui passe à travers les deux.

Couche 3 : injection, en deux endroits que les gens oublient

La plupart des développeurs connaissent l'injection SQL. L'insertion dans D1 utilise une requête paramétrée :

await c.env.DB.prepare('INSERT INTO messages (name, email, intent, body) VALUES (?, ?, ?, ?)')
  .bind(name, email, intent, message)
  .run();

Le texte du visiteur est transmis comme données, jamais collé dans la chaîne SQL, donc un message contenant '); DROP TABLE messages; -- est stocké comme exactement cela : un message étrange.

Le deuxième point d'injection est moins célèbre : les en-têtes de courrier électronique. Le courrier électronique de notification est construit à la main comme un message MIME, et le nom et l'adresse électronique du visiteur se retrouvent dans les Subject et Reply-To en-têtes. Les en-têtes de courrier électronique sont séparés par des sauts de ligne. Si le « nom » d'un visiteur contenait un saut de ligne suivi de Bcc: [email protected], une mise en œuvre négligente ajouterait un nouvel en-tête et transformerait mon serveur en relais de spam. Donc, chaque valeur qui entre dans un en-tête passe par une fonction qui remplace les sauts de ligne par des espaces, et les noms non ASCII (comme Kɔfi ou 李明) sont codés correctement plutôt que transmis bruts.

Le corps du courrier électronique est du texte brut, et non du HTML, donc un message plein de <script> balises est juste du texte lorsque je le lis.

Couche 4 : les en-têtes ennuyeux qui font beaucoup

Le site Web lui-même est servi avec des en-têtes de sécurité stricts : une politique de sécurité du contenu qui n'autorise les scripts que de mon propre domaine et des deux services Cloudflare que j'utilise, frame-ancestors 'none' et X-Frame-Options: DENY contre le clickjacking, HSTS pour que les navigateurs utilisent toujours HTTPS, et un Permissions-Policy qui désactive l'appareil photo, le microphone et la localisation. Rien de tout cela n'est excitant, et c'est le but. La bonne sécurité est principalement des décisions ennuyeuses prises de manière cohérente.

Où cela est encore faible

Un modèle de menace qui ne trouve aucune faiblesse n'est pas un modèle de menace. Voici ceux que je connais :

  • Les limites de débit basées sur l'IP pénalisent les réseaux partagés. De nombreux réseaux mobiles placent un grand nombre d'utilisateurs derrière un petit pool d'adresses IP partagées (NAT de grade de transporteur). Sur un réseau comme celui-ci, un utilisateur abusive pourrait utiliser la limite horaire pour des personnes innocentes qui partagent leur adresse. Pour un site personnel, le compromis est acceptable, mais c'est un véritable problème d'équité pour les services conçus pour les utilisateurs mobiles africains, et c'est un problème auquel je veux réfléchir davantage.
  • La limite de débit est approximative. KV est finalement cohérent, et la lecture puis l'écriture du compteur ne sont pas atomiques, donc un burst rapide de requêtes parallèles peut légèrement dépasser la limite. C'est acceptable pour amortir les spams ; ce ne serait pas acceptable pour, disons, limiter les tentatives de connexion, où j'utiliserais un magasin fortement cohérent.
  • 'unsafe-inline' est toujours dans la politique de script. Le site statique utilise quelques scripts intégrés, donc le CSP les autorise. Cela affaiblit la protection du CSP contre les scripts intersites. La solution appropriée est de passer à des hachages ou des nonces pour chaque script intégré.
  • Les messages sont conservés indéfiniment, en texte brut. Pour le moment, il n'y a pas de politique de rétention. En vertu de la Loi sur la protection des données du Ghana, 2012 (Loi 843), et de bonne pratique partout, je ne devrais conserver les données personnelles que tant que j'en ai besoin. Un travail planifié qui supprime les messages après une période fixe est la prochaine étape évidente.

J'ai classé ces quatre points et j'ai écrit comment je prévois de corriger chacun d'eux dans un article de suivi.

Ce que j'en ai retenu

Deux choses sont restées avec moi.

Tout d'abord, la sécurité commence avec la question « qui est de l'autre côté de cette terminaison ? », et non avec une liste d'outils. Turnstile, KV et les requêtes paramétrées ne sont utiles que parce que le modèle de menace a indiqué où les placer.

Ensuite, les petites fonctionnalités sans gloire sont où se cache le risque. Personne ne teste les formules de contact. Ce sont précisément les systèmes qui semblent trop petits pour être importants, un portail de résultats d'une école, une page de dons d'une église, un formulaire d'inscription d'une petite ONG, qui détiennent des données réelles de personnes avec la moindre protection. Ce sont les systèmes dont je me soucie le plus pour les rendre plus sûrs, et c'est là que je veux que mes recherches aillent.

Si vous repérez quelque chose que j'ai manqué, je serais vraiment ravi de le savoir : contactez-moi. Oui, via le formulaire.

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.