dangerouslySetInnerHTML porte bien son nom : cette propriété React indique explicitement au moteur de rendu de désactiver l’échappement automatique du contenu. Dans un bloc Gutenberg personnalisé, elle apparaît naturellement dès qu’un développeur veut insérer du HTML généré dynamiquement — un extrait enrichi, une liste construite depuis une source externe, un rendu Markdown converti côté client. Le problème survient quand cette valeur provient, même indirectement, d’une saisie utilisateur non filtrée.
La fonction save() d’un bloc génère le HTML statique qui sera enregistré en base de données dans le contenu du post. Contrairement à render_callback côté PHP, ce HTML n’est pas régénéré à chaque affichage : il est figé au moment de la sauvegarde, et WordPress ne le repasse pas systématiquement par wp_kses_post() à l’affichage pour un bloc statique. Une valeur non filtrée à ce stade reste donc présente telle quelle dans le contenu publié.
Le scénario qui pose problème
Prenons un bloc « citation sourcée » qui permet à l’auteur de saisir un texte de citation via un RichText, puis de composer une légende contenant un lien généré dynamiquement à partir d’un champ texte libre. Si la fonction save() construit la légende ainsi :
save() {
const { legende } = attributes;
return (
<div dangerouslySetInnerHTML={ { __html: legende } } />
);
}
et que legende provient d’un champ texte simple (pas d’un RichText qui applique son propre nettoyage), rien n’empêche un contributeur autorisé — ou une source de contenu importée automatiquement — d’y placer une balise script ou un gestionnaire d’événement dans un attribut.
Corriger sans renoncer au HTML enrichi
La première option, la plus sûre, consiste à ne jamais utiliser de champ texte libre pour du contenu destiné à devenir du HTML. Les composants RichText de @wordpress/block-editor gèrent déjà leur propre sérialisation et échappent correctement le contenu qu’ils produisent ; il est presque toujours préférable de les utiliser plutôt que de reconstruire du HTML à la main.

Quand dangerouslySetInnerHTML reste nécessaire — par exemple pour afficher un extrait provenant d’une source externe déjà en HTML — la valeur doit être filtrée avant d’être stockée dans les attributs du bloc, pas seulement au moment de l’affichage.
Filtrer côté client avant la sauvegarde
La bibliothèque @wordpress/dom-purify ou une fonction de nettoyage maison peuvent être appliquées dans l’éditeur, avant que la valeur ne devienne un attribut du bloc :
import { safeHTML } from '@wordpress/dom';
function onChangeExtrait( html ) {
setAttributes( { extrait: safeHTML( html ) } );
}
safeHTML(), exposée par @wordpress/dom, retire les éléments et attributs dangereux d’une chaîne HTML côté client. Elle n’est pas un filtre exhaustif au niveau serveur, mais elle réduit fortement la surface d’attaque au moment de la saisie.
Ne jamais faire confiance uniquement au filtrage client
Un filtrage exécuté uniquement dans le navigateur reste contournable : un attaquant qui appelle directement l’API REST des articles, ou qui importe du contenu via un flux WXR, n’exécute jamais le code JavaScript de l’éditeur. C’est pourquoi un filtre serveur reste nécessaire, appliqué au moment de la sauvegarde du post :
add_filter( 'content_save_pre', function ( $content ) {
return wp_kses_post( $content );
} );
Ce filtre s’applique au contenu global du post, y compris le HTML statique produit par save(), et retire les balises et attributs non autorisés par la liste par défaut de wp_kses_post().
Tester avec des charges hostiles avant publication
Avant de valider un bloc qui manipule du HTML dynamique, il vaut mieux le tester volontairement avec des valeurs conçues pour révéler une faille : une chaîne contenant <img src=x onerror=alert(1)>, un attribut href en javascript:, ou une balise svg avec un gestionnaire embarqué. Si l’un de ces éléments survit à l’enregistrement du post et apparaît côté public, le filtrage est insuffisant.
- Tester une charge dans un champ texte simple, pas seulement dans un RichText
- Vérifier le HTML réellement enregistré en base, pas seulement l’aperçu dans l’éditeur
- Rejouer le test après chaque modification du bloc, un correctif partiel pouvant réintroduire le problème ailleurs
Ce qu’il faut retenir
Utiliser dangerouslySetInnerHTML dans save() n’est pas une erreur en soi ; l’erreur consiste à l’alimenter avec une valeur qui n’a jamais été filtrée, ni côté client au moment de la saisie, ni côté serveur au moment de l’enregistrement. Préférer les composants RichText quand c’est possible, et combiner un nettoyage client avec un filtre serveur systématique, ferme la quasi-totalité des scénarios d’injection observés sur ce type de bloc.