Blog / Ingénierie / No.008

Détecter un formulaire de contact sans rien casser

Une porte est une voie de contact publique utilisable. En trouver une sur un site quelconque, c'est de l'archéologie de plateformes, une fonction de notation, et deux gardes qui doivent échouer côté fermé.

Mohamad · Knockwire16 juin 202612 min de lectureIngénierie

Ce texte est une traduction de l'original anglais.

La détection de formulaire de contact ressemble à un problème résolu jusqu'à ce que vous la pointiez sur un millier de sites d'entreprises. Les échecs n'ont rien d'exotique. Un formulaire est là et l'analyseur ne le voit pas. L'analyseur voit neuf champs et écrit le message dans le mauvais. Le garde censé nous arrêter sur un formulaire protégé lit une propriété qui n'existe pas, reçoit undefined, et laisse tout passer. Nous appelons porte une voie de contact publique utilisable, et voici ce qu'en trouver une implique réellement.

01La détection de formulaire de contact commence par la plateforme

La bonne nouvelle d'abord : les formulaires de contact ne forment pas une longue traîne. Sur les entreprises que nous lisons, cinq choses représentent l'essentiel de ce qui apparaît. HubSpot, ActiveCampaign, WPForms, Typeform, et un formulaire HTML simple qui poste vers le site où il se trouve. Identifiez d'abord la plateforme, puis exécutez l'extracteur écrit pour elle. Un passage générique qui ramasse chaque champ de la page se trompe sur au moins trois des cinq, et se trompe sans rien signaler.

  • HubSpot : la page porte un identifiant de portail et un guid de formulaire, et rien de ce que vous soumettez ne va vers la page que vous regardez. La soumission est un POST JSON vers un point d'accès régional indexé sur le portail, et la région fait partie de l'adresse plutôt que d'un en-tête : un portail européen soumis vers l'hôte par défaut n'est accepté par personne. La liste des champs vient de la définition du formulaire, pas du DOM.
  • ActiveCampaign : l'intégration est une balise de script, et le balisage vit à l'intérieur d'une chaîne JavaScript, doublement échappée. Les chevrons arrivent sous forme d'échappements unicode et chaque guillemet est précédé d'une barre oblique inverse. Un analyseur HTML pointé sur cette page ne trouve aucun formulaire, parce qu'à cet instant il n'y en a pas. Déséchappez la chaîne, analysez le résultat, puis extrayez depuis le résultat.
  • WPForms : les noms sont positionnels. Un champ s'appelle wpforms[fields][3] et le 3 est un identifiant de ligne dans la base WordPress de quelqu'un d'autre, si bien que le nom ne porte aucun sens. Le texte du libellé et l'attribut de type du champ le portent à sa place.
  • Typeform : il n'y a pas de formulaire sur la page. Il y a un parcours hébergé décrit en blocs JSON, et la lecture honnête est que ce n'est pas une porte que nous puissions emprunter.
  • HTML simple : une action, une méthode, et le plus souvent un jeton caché qu'il faut renvoyer avec le cookie de session qui l'a émis. La récupération et la soumission doivent partager un même bocal à cookies, sinon la publication est rejetée comme une falsification, ce qui est le bon comportement de la part du site.

Chacun de ces cas est un contrat de soumission plutôt qu'une bizarrerie de rendu. L'extracteur de l'un n'est pas une version légèrement ajustée de l'extracteur de l'autre, et prétendre le contraire produit un détecteur qui déclare le succès sur presque chaque page et un taux de délivrance qui ne lui donne pas raison.

02La mise en correspondance des champs, là où cela devient difficile

Admettons que le formulaire soit trouvé et analysé. Vous tenez maintenant entre quatre et onze champs et un message à placer. Quelle case est le message ?

Nous notons chaque champ sur le nom, l'identifiant, l'indication, le texte du libellé associé, aria-label, le type d'élément et maxlength, puis nous prenons le meilleur score au-dessus d'un seuil. Une zone de texte est le signal isolé le plus fort et n'est pourtant pas décisive : beaucoup de formulaires utilisent une zone de texte pour une adresse postale, et certains utilisent un champ d'une seule ligne avec un maxlength de 500 pour le message. Objet et message sont la paire qui se trompe le plus souvent. Les deux sont du texte, ils sont voisins, et les intervertir donne au destinataire une ligne d'objet longue de quatre paragraphes.

Il y a ensuite les champs auxquels il ne faut pas toucher. Un pot de miel est un champ caché à une personne et laissé visible à un script naïf, et le remplir est précisément la façon dont le site apprend que vous êtes un script. Nous marquons un champ comme pot de miel sur l'un de ces critères : caché par display ou visibility, positionné hors écran, opacité nulle, tabindex négatif, aria-hidden, ou un nom pris dans le jeu habituel (url, website, hp, _gotcha, ou comment lorsqu'un champ de commentaire existe déjà). Un champ marqué est soumis exactement tel que nous l'avons trouvé. Ce n'est pas un contournement de quoi que ce soit. C'est remplir le formulaire comme le remplirait le navigateur devant une personne.

En dessous du seuil, nous ne devinons pas. Il n'y a pas de porte, la lecture est mise en attente avec le motif consigné, et elle apparaît dans l'index des refus comme refusée plutôt que comme une tentative discrète. Poster un paragraphe dans le champ intitulé « Comment nous avez-vous connus » est pire que de ne rien poster, parce que cela dépense l'attention d'un inconnu à l'intérieur d'un formulaire auquel il ne peut pas répondre.

Un détecteur incapable de dire « je ne sais pas » trouvera toujours une porte, y compris sur les sites qui n'en ont pas.

03Deux bugs silencieux exactement de la mauvaise manière

Le premier est la pire chose que nous ayons livrée. Le détecteur renvoie un résultat imbriqué et la détection du CAPTCHA se trouve à result.form.captcha. Le garde lisait result.captcha. Cette propriété est undefined, undefined est falsy, et le garde en concluait donc qu'aucun formulaire nulle part n'avait de CAPTCHA. Rien n'a levé d'erreur. Rien n'a été journalisé. Des formulaires protégés sont passés pendant presque tout un après-midi, et cela n'a émergé que parce qu'une vérification manuelle sur un formulaire que nous savions protégé est revenue propre et que personne n'a cru le résultat.

Le correctif est une règle plutôt qu'un chemin de propriété corrigé. Le garde exige désormais un false explicite pour poursuivre. Tout le reste met la tâche en attente : undefined, null, un objet manquant, une chaîne, une valeur vraie. Nous vérifions la forme du résultat du détecteur à la frontière au lieu de le traverser avec optimisme, et il existe un test qui remet au garde un résultat dont le drapeau a été déplacé d'un niveau et qui attend une mise en attente. Il échoue contre l'ancien code, ce qui est la seule définition utile d'un test de non-régression.

MéthodeLes gardes échouent côté fermé. Un contrôle de sécurité qui lit une valeur absente doit traiter l'absence comme le cas dangereux, parce qu'il y a deux façons d'obtenir une valeur absente : un site sans CAPTCHA, et un détecteur que vous avez cassé. Une seule des deux est sans danger, et le contrôle ne sait pas les distinguer.

Le second bug est plus petit et a coûté plus d'heures. La correspondance de champs autorisait une correspondance par suffixe, si bien qu'un champ nommé contact[email] et un champ nommé contact[secondary_email] satisfaisaient tous deux une règle cherchant un champ email, et le gagnant était celui qui venait en dernier dans l'ordre du document. Sur un formulaire où l'adresse secondaire était facultative et non validée, nous avons appliqué un champ obligatoire sur le champ facultatif. La publication a réussi, le site a renvoyé une page de remerciement, et le message a atterri dans un champ que personne ne lit.

Trois règles tranchent désormais, appliquées dans l'ordre. Une correspondance exacte de nom l'emporte sur une correspondance par suffixe et met fin à la recherche. Obligatoire l'emporte sur facultatif, lu depuis l'attribut required, depuis aria-required, et depuis la définition de champ de la plateforme quand la plateforme en fournit une. Les égalités vont à la position la plus tôt dans le document. La correspondance obtenue est écrite dans l'enregistrement d'audit, pour que la personne qui valide le brouillon puisse voir dans quel champ chaque valeur atterrira avant de valider plutôt qu'après.

04Là où nous nous arrêtons

Nous ne résolvons ni ne contournons jamais un CAPTCHA. Ni avec un service de résolution, ni avec un navigateur réglé pour paraître plus humain, ni en cherchant le point d'accès hérité non protégé qui se trouve derrière celui qui l'est, ce qui est le même acte accompli avec de meilleures manières. Quand le détecteur signale un CAPTCHA, la tâche passe dans needs_review et y reste jusqu'à ce qu'une personne l'ouvre. La plupart du temps, personne ne le fait, et c'est un résultat plutôt qu'un échec.

Un CAPTCHA sur un formulaire de contact est une préférence exprimée. Le propriétaire a dit, dans à peu près le seul vocabulaire disponible pour le dire à un logiciel, qu'il veut une personne à l'autre bout. Notre métier consiste à demander à des inconnus d'envisager un achat, et cette conversation ne s'ouvre pas bien en démontrant que leurs préférences ne nous lient pas.

Quelques autres choses ne se qualifient pas comme portes. Un formulaire derrière une authentification n'est pas public. Un formulaire de ticket de support est le mauvais public, parce que c'est une file dotée d'effectifs pour des clients payants et qu'un message commercial y coûte son après-midi à quelqu'un. Une inscription à une lettre d'information n'est pas une voie de contact. Chacune est consignée comme une lecture refusée avec un motif écrit, le même traitement qu'une entreprise reçoit quand elle échoue à une étape d'adéquation.

La délivrance passe aujourd'hui par le formulaire de contact public de la cible et par rien d'autre. L'envoi d'emails est prévu pour le quatrième trimestre 2026 et n'est pas construit, si bien qu'il n'existe aucune voie de repli quand une porte manque et aucun second canal quand le premier est protégé. Cette absence est ce qui garde la règle honnête. Si une alternative facile existait, mettre la tâche en attente ne coûterait rien, et une règle qui ne coûte rien ne vous apprend rien.

Rien de tout cela n'est astucieux. C'est un détecteur de plateforme, une fonction de notation, deux gardes qui échouent côté fermé, et un motif écrit attaché à chaque lecture sur laquelle nous n'avons pas agi. La version astucieuse de ce système afficherait un nombre d'envois effectués plus élevé et serait pire.

Knockwire lit le web, écarte les entreprises qui n'achèteront jamais chez vous, et frappe à la porte de celles qui le feront. Lancez-le sur votre propre domaine et lisez vos propres refus.

L'essayer sur mon siteTous les textes