# Neutraliser une charge utile Unicode qui contourne un filtre de mots interdits par normalisation

> Un filtre de mots interdits qui compare des chaînes brutes laisse passer des variantes Unicode : la normalisation avant comparaison change tout.

- Auteur : Clément Hadrot
- Publié le : 2024-12-03
- Mis à jour le : 2024-12-03
- Catégorie : Sécurité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/securite/neutraliser-charge-utile-unicode-filtre-mots-interdits/

## L’essentiel

- Un mot interdit peut se réécrire avec des caractères visuellement identiques
- La normalisation Unicode doit précéder le filtrage, jamais le suivre
- Un test unitaire sur des variantes homoglyphes évite la régression

`ﬁltre` 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 `ﬁltre` 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.

> L'essentiel à retenir : Un mot interdit peut se réécrire avec des caractères visuellement identiques ; La normalisation Unicode doit précéder le filtrage, jamais le suivre ; Un test unitaire sur des variantes homoglyphes évite la régression

```
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',
        'ﬁltre',        // ligature U+FB01
        'fīltre',       // i avec macron
        'ﬀiltre',       // 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.
