# Blocs vulnérables : XSS via les attributs et render.php

> Un attribut de bloc affiché sans échappement dans render.php, ou du HTML injecté par save(), suffisent à ouvrir une faille XSS que les revues classiques repèrent mal.

- Auteur : Clément Hadrot
- Publié le : 2024-11-11
- Mis à jour le : 2024-11-11
- Catégorie : Sécurité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/securite/blocs-vulnerables-xss-attributs-render-php/

## L’essentiel

- Un attribut de bloc n'est jamais une donnée de confiance
- render.php doit échapper comme n'importe quel template PHP
- save() qui injecte du HTML brut est une porte ouverte

En auditant une extension qui ajoutait un bloc « citation mise en avant » à l'éditeur, nous avons trouvé un attribut `auteur` affiché directement dans `render.php` via `echo $attributes['auteur'];`, sans la moindre fonction d'échappement. N'importe quel utilisateur autorisé à éditer du contenu, même un simple contributeur, pouvait donc injecter du HTML ou du JavaScript arbitraire dans ce champ, exécuté ensuite pour chaque visiteur du site.

Les blocs Gutenberg introduisent deux points d'entrée spécifiques que les revues de sécurité classiques, habituées aux formulaires et aux entrées AJAX, ont parfois tendance à sous-estimer : les attributs de bloc côté `render.php`, et le HTML généré par la fonction `save()` côté client. Voici ce qui s'y passe et comment le corriger.

## Ce qu'on voit : un attribut affiché sans échappement

Le code fautif typique ressemble à ceci, dans le fichier `render.php` d'un bloc dynamique :

```
<div class="ma-citation">
    <p><?php echo $attributes['texte']; ?></p>
    <cite><?php echo $attributes['auteur']; ?></cite>
</div>
```

Rien ne distingue visuellement ce fichier d'un template de thème classique, ce qui explique en partie pourquoi il échappe à la vigilance : les développeurs habitués à échapper les sorties dans `content.php` ou `single.php` oublient parfois d'appliquer la même rigueur dans `render.php`, qui semble « appartenir » à l'éditeur plutôt qu'au front-end public.

## Pourquoi c'est un problème même avec des rôles limités

> L'essentiel à retenir : Un attribut de bloc n'est jamais une donnée de confiance ; render.php doit échapper comme n'importe quel template PHP ; save() qui injecte du HTML brut est une porte ouverte

L'argument « seuls les auteurs de confiance peuvent éditer du contenu » ne tient pas dans de nombreux contextes réels : un site avec inscription ouverte aux contributeurs, un site multi-auteurs avec des comptes externes, ou même un compte auteur légitime mais compromis via un mot de passe faible, suffisent à rendre cette faille exploitable. De plus, un contenu XSS stocké dans un attribut de bloc s'exécute pour **tous les visiteurs** qui consultent la page publiée, pas seulement pour l'auteur qui l'a saisi, ce qui démultiplie l'impact par rapport à une XSS réfléchie limitée à une seule victime par tentative.

## Le correctif dans render.php

La règle ne change pas par rapport à n'importe quel autre template WordPress : chaque sortie doit être échappée selon son contexte d'affichage, en utilisant la fonction adaptée :

```
<div class="ma-citation">
    <p><?php echo esc_html( $attributes['texte'] ); ?></p>
    <cite><?php echo esc_html( $attributes['auteur'] ); ?></cite>
</div>
```

Si l'attribut doit conserver un minimum de mise en forme (gras, lien), `wp_kses_post()` filtre le HTML autorisé selon la liste blanche standard des articles, plutôt que d'autoriser n'importe quelle balise :

```
<p><?php echo wp_kses_post( $attributes['texte'] ); ?></p>
```

## Ce qu'on voit côté save() : du HTML injecté sans contrôle

Le second point d'entrée concerne les blocs statiques, dont le rendu HTML est généré côté client par la fonction `save()` en JavaScript et enregistré tel quel dans le contenu de l'article. Un bloc qui construit dynamiquement son balisage à partir d'un attribut texte, sans passer par les composants d'échappement fournis par `@wordpress/element`, peut également ouvrir une XSS :

```
// Antipattern : insertion de HTML brut non contrôlé
save: ( { attributes } ) => {
    return (
        <div dangerouslySetInnerHTML={ { __html: attributes.texte } } />
    );
},
```

`dangerouslySetInnerHTML`, comme son nom le suggère explicitement dans l'API React sous-jacente, insère le contenu tel quel dans le DOM sans aucun échappement. Utilisé sur un attribut librement saisi par un auteur de contenu, c'est un vecteur XSS direct, stocké de façon permanente dans le contenu de l'article une fois publié.

## Pourquoi c'est un problème

Contrairement au rendu dynamique de `render.php`, qui s'exécute à chaque affichage et peut donc être corrigé rétroactivement en modifiant simplement le fichier PHP, le HTML généré par `save()` est figé dans le contenu de l'article au moment de la sauvegarde. Corriger la fonction `save()` après coup ne nettoie pas automatiquement le contenu déjà enregistré : il faut alors une migration explicite du contenu existant, ce qui rend cette catégorie de faille plus coûteuse à corriger une fois découverte en production.

## Le correctif côté save()

Le composant standard `RichText.Content` de `@wordpress/block-editor`, conçu spécifiquement pour ce cas d'usage, applique un filtrage cohérent avec celui utilisé par l'éditeur lui-même :

```
import { RichText } from '@wordpress/block-editor';

save: ( { attributes } ) => {
    return <RichText.Content tagName="p" value={ attributes.texte } />;
},
```

Pour un attribut de type simple texte sans mise en forme, préférez un attribut de type `string` affiché comme du texte pur, sans jamais passer par un rendu HTML brut construit à la main.

## Ce qu'on vérifie désormais systématiquement

- Recherche de `echo $attributes` sans fonction d'échappement dans tous les fichiers `render.php` du projet.
- Recherche de `dangerouslySetInnerHTML` dans le code source JavaScript des blocs, avec vérification de l'origine de la valeur injectée.
- Test manuel : saisir `<script>alert(1)</script>` dans chaque champ de bloc avant mise en production, et vérifier son rendu côté front-end public.

> Ce test du `<script>alert(1)</script>` saisi dans chaque champ texte reste, malgré son apparence artisanale, le moyen le plus rapide de repérer une XSS dans un bloc avant qu'elle n'atteigne la production.

## Pour aller plus loin

La documentation officielle sur la [sécurisation des blocs](https://developer.wordpress.org/block-editor/how-to-guides/secure-your-block/) détaille les fonctions d'échappement recommandées côté PHP et JavaScript. Elle mérite une lecture attentive avant tout développement de bloc dynamique destiné à un site en production.
