Développer un plugin de détection de spams pour CaMykS
Après la création d'un plugin de géolocalisation, il y a quelques mois, je m'étais lancé dans le développement d'un plugin de détection des spams pour CaMykS, le CMS utilisé pour skymac.org. Les deux plugins sont d'ailleurs un peu liés. En effet, le plugin de géolocalisation a été créé pour être indépendant, utilisant dans de nombreux buts, mais a été développé pour être l'un des points de vérification du détecteur de spams.
1/ Définition des besoins
La première chose à faire est de définir un cahier des charges afin de développer l'outil correspondant aux besoins.
Dans mon cas, le premier pré-requis était de pouvoir l'appeler depuis n'importe quel outil de gestion de formulaire. Qu'il s'agisse d'un formulaire de contact ou d'inscription, en passant par une gestion de commentaires, il faut pouvoir s'en servir partout.
Basé sur mon expérience des spams sur skymac.org et des nombreux autres sites dont je m'occupe, le second pré-requis est d'avoir une solution entièrement configurable. L'outil doit pouvoir prendre en charge de nombreuses règles, sans pour antant qu'elles soient toutes actives ou avec les mêmes réglages, car les besoins pour un site peuvent être différents de ceux d'un autre.
Mieux, appliquer une pondération sur les différentes règles permettrait aux visiteurs de ne pas voir leur messages refuser, sauf en cas d'excès.
Enfin, en entrée, il faut pouvoir indiquer les informations à vérifier sans pour autant qu'elles soient toutes obligatoires. En effet, les données d'un commentaire ne seront pas les mêmes que pour un formulaire de contact.
Maintenant que nous avons posé les besoins sur le papier, nous pouvons entrer un peu plus dans le détail.
2/ Définition des règles
Avant de me lancer à corps perdu dans le développement du plugin, j'ai étudié les nombreux spams qui venaient polluer les sites dont je m'occupe. J'ai ainsi commencé une liste des règles de détection qu'il fallait que l'outil prenne en charge. Pendant plusieurs semaines, j'ai noté des idées dans un fichier constamment ouvert.
Voici règles sur lesquelles j'ai fini par statuer, avec un peu plus de détails pour certaines
- Vérification des noms et prénoms : il s'agit d'un facteur caractéristique pour détecter un spam, lorsque les noms et prénoms sont identiques, ou identiques avec 2 caractères supplémentaire en fin de nom tel que "Daviddiz DaviddizNM".
- Vérification de la présence de HTML dans le message
- Vérification de la présence de Javascript dans le message
- Vérification des adresses emails : certains patterns reviennent souvent dans les adresses e-mails. Aussi mettre un petit score sur les adresses @gmail.com, @hotmail.com ou outlook.com, principalement utilisées pour les spams, permet d'accentuer la détection réalisée par d'autres règles.
- Vérification du nom de la société : dans les formulaires de contact, les spams utilisent beaucoup le nom "google", il s'agit d'un des moyens de les détecter.
- Vérification de la présence de liens : bien souvent, les spams cherchent à publier des liens, soit pour que les visiteurs y aillent, soit pour augmenter le référencement de leurs sites. Cependant, un visiteur peut aussi partager des lignes. Il faut donc la configurer avec précaution.
- Vérification de la présence de certains mots : c'est un classique, mais indiquer une liste de mots souvent utilisés dans les spams est un point clé de la détection.
- Vérification de la présence de scripts de langages : recevoir des messages écrits en cyrillique, arabe ou chinois sur un site ne fournissant que du contenu en français, peut faciliter la détection de spams.
- Vérifier le pays d'origine du message : les spams que j'ai pu recevoir proviennent principalement de Russie, d'Ukraine et de Chine. Vérifier la géolocalisation du message peut compléter la vérification. Attention, 100% des messages venant de ces pays ne sont pas tous des spams, et 100% des spams ne viennent pas non plus de ces pays. Il faut donc faire attention dans la configuration.
- Vérifier le nom d'hôte : en plus de la géolocalisation basée sur l'adresse IP, cette dernière peut aussi nous indiquer le nom d'hôte depuis lequel le message provient. Lorsque le nom d'hôte est un fournisseur d'accès à Internet tel que Orange ou Free, il y a de grandes chances pour que le message ait été posté par une vraie personne. Par contre, si le message provient d'un serveur situé chez un hébergeur connu, tel qu'OVH ou Online. Il y a de bonne chance que le message provienne d'un Joomla ou d'un site utilisant un autre CMS, s'étant fait hacké.
Nous avons fait le tour des règles que je souhaitais implémenter. Evidemment, il est possible d'en imaginer d'autres, les commentaires sont disponibles pour vos idées.
3/ Création du plugin - Partie administrative
Une fois que les spécifications sont faites, le plus gros du travail est fait, puisqu'il ne reste plus à transformer en code, les réflexions posées sur le papier.
Comme pour le plugin de géolocalisation, je ne vais pas rentrer les détails, spécifiques au CMS. Ce qu'il faut retenir, c'est que le plugin doit être accessible depuis l'espace de configuration du site.
Voici ce que cela donne la configuration du plugin dans le CMS
En tête de page, nous pouvons y avoir deux réglages dont nous n'avons pas encore parlé.
Le premier est la journalisation des détections. Il est possible de ne pas enregistrer les détections, ce qui n'est pas franchement recommandé. Il est possible d'activer l'enregistrement qui va indiquer les informations minimales, mais surtout permet de voir à chaque fois qu'un message est traité par le plugin. Enfin, et c'est le cas ici, tant que je suis en phase de test, je préfère tout enregistrer, le message ainsi que toutes les informations qui y sont associées.
Le second point est peut être le plus important, il s'agit de définir le score à atteindre pour qu'un message soit signalé comme spams. Petite subtilité supplémentaire, j'ai ajouté un second score pour les messages qui sont des spams probables.
Ensuite, nous retrouvons tous les points cités au dessus. Certains sont simplement des cases à cocher pour activer la règle. Tandis que d'autres permettent la construction d'une liste d'éléments. A chaque fois, nous trouvons en face, le score pour chacun des points de détection.
4/ Création du plugin - Execution de la détection
Puis, la détection doit pouvoir être appelée par n'importe quel autre plugin qui le souhaite. Le principe sera le même que pour la géolocalisation. Le plugin ne dispose que d'une méthode principale "execute" qui permet de demander une détection.
public function execute($params=array())
Cette méthode nécessite quelques paramètres, à savoir le mode de sortie, qui pour l'instant n'est limité qu'au résultat, une courte description de l'objet qui doit être testé, puis les données.
Comme les données ne sont obligatoires, il est nécessaire de fusionner les données avec une liste vide pour être sur que toutes les données apparaissent dans la liste.
$defaultParams = array(
'output' => 'result',
'object' =>'',
'objectId' => '',
'data' => array(
'fullName' => '',
'firstName' => '',
'lastName' => '',
'email' => '',
'company' => '',
'phone' => '',
'content' => '',
'ipAddress' => '',
'hostname' => '',
),
);
$params = array_recursiveMerge($defaultParams, $params);
La fonction array_recursiveMerge est une fonction du CMS qui permet de fusionner récursivement des arrays. Elle est très pratique pour fusionner des listes de paramètres à plusieurs niveaux.
Il est ensuite nécessaire de charger la configuration du détecteur, mise en place dans la partie 3
$this->load_configuration();
J'appelle ensuite une fonction qui permet de vérifier les paramètres
$params = $this->check_params($params);
Cette fonction va permettre de récupérer l'adresse IP si elle n'est pas envoyée dans les données, ainsi que le hostname correspondant.
/**
* Check parameters.
* @param array $params
* @return array
*/
private function check_params($params) {
global $camyks;
/* Check IP address */
if (empty($params['data']['ipAddress']) or !string_isValidIPv4($params['data']['ipAddress']))
$params['data']['ipAddress'] = client_getIp();
/* Add hostname from IP address */
if ($params['data']['hostname'] === '' and ($v = @gethostbyaddr($params['data']['ipAddress'])) !== $params['data']['ipAddress']) {
$params['data']['hostname'] = $v;
}
/* Return updated params */
return $params;
}
Enfin, on rentre dans le vif du sujet, en testant si les données fournies peuvent être considérées comme un spam
return $this->check_contentIsSpam($params);
Regardons maintenant, cette méthode qui est le coeur de notre outil.
Cette methode va prendre les paramètres puis les traiter, mais avant, il faut définir les valeurs de résultat.
/**
* Check content is spam.
* @param array $params
* @return mixed
*/
private function check_contentIsSpam($params) {
global $camyks;
/* Initialise score result */
$scores = array('score' => 0, 'result' => 'isNotSpam', 'data' => array());
Ensuite, viennent tous les tests de détection. Comme ils sont tous bien différents, avec des spécificités qui leurs sont propres, il n'est pas possible de factoriser les tests. Ils ont donc tous été écrits un par un.
Pour le premier, la vérification des noms, voici ce que cela donne
if ($this->get_configValue('checkSimilarNamesEnabled')) {
$firstName = '';
$lastName = '';
if ($params['data']['fullName'] !== '') {
$name = explode(' ', $params['data']['fullName']);
if (count($name) === 2) {
$firstName = trim($name[0]);
$lastName = trim($name[1]);
}
} else {
$firstName = trim($params['data']['firstName']);
$lastName = trim($params['data']['lastName']);
}
if ($firstName !== '' and $lastName !== '') {
if ($firstName == $lastName and preg_match('/^'.string_escape4Regex($firstName).'[A-Za-z]{2}$/', $lastName) or preg_match('/^'.string_escape4Regex($lastName).'[A-Za-z]{2}$/', $firstName)) {
$scores['score'] += $this->get_configValue('checkSimilarNamesScore');
$scores['data'][] = 'Similar names: '.$this->get_configValue('checkSimilarNamesScore');
}
}
}
Nous commençons par vérifier que la règle est bien active. Puis nous récupérons les noms et prénoms. S'ils ont été fournis séparément, c'est assez simple, par contre, nous permettons aussi l'envoi de ces données en un seul bloc, il faut donc séparer les deux du mieux possible.
Ensuite, nous vérifions que nous avons bien un nom et un prénom, car dans le cas contraire, il est inutile de faire les tests.
Il ne reste plus qu'à faire les tests. Nous vérifions d'abord si les deux sont identiques, c'est le test le moins couteux et le plus fréquent. Même si cela ne fait gagner que quelques millionièmes de seconde, l'optimisation du code reste un point clé du développement. Ensuite, nous effectuons les deux expressions régulières pour trouver les noms identiques avec deux caractères supplémentaires.
Si l'un des tests est validé, nous mettons à jour le score et enregistrons l'action dans un liste "data" à part.
Je ne vais pas détailler tous les points de contrôle, car ils sont finalement nombreux, et construits de la même manières. Je pourrais toujours répondre à vos questions si certains points de contrôle restent obscures.
Une fois les tests faits, il faut vérifier si le score correspond à un spam ou à un peut être spam.
if ($scores['score'] > $this->get_configValue('isSpamScore'))
$scores['result'] = 'isSpam';
elseif ($scores['score'] > $this->get_configValue('isProbablySpamScore'))
$scores['result'] = 'isProbablySpam';
Maintenant que les données de sortie sont finalisées, il faut effectuer la journalisation des informations, avec les deux niveaux : simple et détaillé
if ($this->get_configValue('enableLogs') > 0) {
$camyks->log('SpamChecker', 'Spam check on object "'.$params['object'].' #'.$params['objectId'].'"', 'Result: '.$scores['result'].' - Score:'.$scores['score']);
if ($this->get_configValue('enableLogs') == 2) {
foreach ($scores['data'] as $line)
$camyks->log('SpamChecker', 'Spam check on object "'.$params['object'].' #'.$params['objectId'].'"', '|- '.$line);
}
}
Puis l'on renvoit le résultat
if ($params['output'] == 'result')
return $scores['result'];
Les différentes valeurs que peut retourner l'outil sont "isNotSpam", "isProbablySpam", "isSpam". Elles sont suffisamment explicites pour ne pas avoir à les détailler.
5/ Réflexions finales
Comme vous pouvez le voir l'outil ne sert vraiment à la détection. En aucun cas, il ne prend de décision sur ce qu'il doit advenir du message transmis.
D'une part, ce n'est pas son rôle, chaque élément doit resté cantonné à sa propre tâche. D'autre part, il faudrait que le présent plugin connaissent tout ceux qui lui envoient des données pour faire un appel à la suppression. Hors ici, c'est l'inverse, il ne connait personne, mais répond à tout ceux qui le sollicitent.
Enfin, cela permet aussi que chaque module l'appellant puisse prendre ses propres décisions. Par exemple, avec un module qui gère des commentaires, il pourrait être configuré tel que
- accepter directement les messages qui ne sont pas des spams
- mettre en attente les messages qui sont probablement des spams
- refuser les messages qui sont des spams avérés
Sur skymac.org, les réglages ne sont pas fait comme cela, car nous souhaitons garder un contrôle même sur les messages qui ne sont pas des spams, afin d'éviter tout débordement.
Vous pouvez retrouver le code source de ce plugin dans le GitHub du CMS, car il fait désormais partie intégrante de ce dernier. Vous y trouverez tous les détails des tests, ainsi que la manière dont sont structurées les données de configuration, même si elles sont spécifiques à CaMykS.
En espérant que ce petit bout de réflexion ait éclairé, avec quelques idées, vos futurs développements.