filtre ressemble à filtre, mais ce n’est pas la même chaîne : le premier utilise une ligature Unicode (U+FB01) là où le second utilise deux lettres latines ordinaires. Un filtre de mots interdits qui compare du texte brut à une liste noire, sans normaliser au préalable, laisse passer ce genre de substitution sans même s’en rendre compte.
Ce problème touche tout formulaire soumis par des visiteurs : commentaires, candidatures, messages de contact. Dès qu’une liste de termes bannis intervient (insultes, marques déposées, tentatives de phishing), un attaquant motivé teste des variantes graphiques pour glisser entre les mailles. La correction ne demande pas une réécriture complète du filtre, mais l’ajout d’une étape de normalisation en amont.
Pourquoi la comparaison brute échoue
PHP compare des chaînes d’octets, pas des idées. Une fonction comme str_contains() ou une expression régulière avec preg_match() traite filtre et filtre comme deux séquences d’octets totalement différentes, même si l’œil humain n’y voit aucune différence à l’écran. Le même principe s’applique aux caractères cyrilliques qui ressemblent à des lettres latines, aux caractères combinants qui ajoutent des accents invisibles, ou aux variantes de largeur (texte pleine largeur utilisé dans certains contextes asiatiques).
Un attaquant qui connaît la liste de mots filtrés (parfois visible dans le code source d’une extension open source) n’a qu’à remplacer une ou deux lettres par leur équivalent visuel pour passer le filtre. Le contenu reste lisible pour un humain, mais invisible pour la machine.
Normaliser avant de comparer
La classe Normalizer de l’extension PHP intl expose les quatre formes de normalisation Unicode : NFC, NFD, NFKC et NFKD. Pour neutraliser les homoglyphes et les ligatures, la forme de compatibilité NFKC est la plus pertinente : elle décompose les caractères en leurs équivalents canoniques avant de les recomposer, ce qui fait disparaître la plupart des substitutions visuelles.

if (!extension_loaded('intl')) {
// Prévoir un repli si intl n'est pas chargé sur l'hébergement
error_log('Extension intl manquante : normalisation Unicode indisponible');
} else {
$entree = wp_unslash($_POST['message'] ?? '');
$normalisee = Normalizer::normalize($entree, Normalizer::FORM_KC);
foreach ($mots_interdits as $mot) {
if (mb_stripos($normalisee, $mot) !== false) {
wp_die(esc_html__('Contenu refusé par le filtre.', 'mon-plugin'));
}
}
}
Ce filtre s’applique sur la chaîne normalisée, jamais sur la chaîne brute affichée à l’utilisateur : la normalisation sert uniquement à la décision de filtrage, la donnée stockée reste inchangée (ou passe par sanitize_textarea_field() selon le contexte).
Les pièges d’une normalisation mal placée
Trois erreurs reviennent souvent dans les correctifs improvisés :
- Normaliser après le filtrage plutôt qu’avant, ce qui ne sert strictement à rien
- Normaliser uniquement le mot interdit et pas l’entrée utilisateur, ou l’inverse
- Oublier les caractères combinants (accents détachés) qui échappent parfois à NFKC seule et nécessitent un retrait explicite des marques diacritiques via
Normalizer::normalize()suivi d’une suppression des catégories Unicode de type marque
Tester avec des cas réels d’homoglyphes
Un test unitaire qui ne couvre que le mot interdit écrit normalement ne prouve rien. Il faut construire un jeu de données avec des variantes réelles : lettres cyrilliques ressemblant à du latin (а au lieu de a), ligatures typographiques, caractères pleine largeur. PHPUnit permet d’itérer sur un tableau de variantes et de vérifier que chacune déclenche le filtre.
public function test_filtre_resiste_aux_homoglyphes(): void
{
$variantes = [
'filtre',
'filtre', // ligature U+FB01
'fīltre', // i avec macron
'ffiltre', // ligature ff
];
foreach ($variantes as $entree) {
$this->assertTrue(
mon_filtre_detecte($entree),
"La variante '{$entree}' aurait dû être détectée"
);
}
}
Un filtre de mots interdits sans normalisation Unicode documentée dans son code n’a probablement jamais été testé contre une charge utile réelle : c’est un signal d’alerte à chercher systématiquement lors d’une revue.
Où placer cette étape dans une extension WordPress
Le bon point d’accroche dépend du contexte : pour un formulaire de commentaire, le filtre pre_comment_content permet d’intervenir avant l’enregistrement. Pour un champ personnalisé sur un formulaire de contact, l’interception se fait dans le gestionnaire de soumission avant l’appel à wp_insert_post() ou à l’API équivalente de l’extension de formulaire utilisée. Dans tous les cas, la normalisation doit intervenir immédiatement après la récupération de l’entrée brute, avant toute autre logique métier qui pourrait dépendre du filtrage.
En résumé
Un filtre de mots interdits qui compare des octets bruts protège contre les tentatives naïves, mais pas contre un attaquant qui connaît l’existence de la normalisation Unicode. Ajouter une étape Normalizer::normalize() en forme NFKC avant la comparaison, tester avec de vrais homoglyphes, et documenter ce choix dans le code sont trois réflexes qui coûtent peu et ferment une classe entière de contournements.