# Analyse de sécurité automatisée du code WordPress avec Semgrep

> Écrire des règles Semgrep pour traquer les échappements manquants, les requêtes SQL non préparées et les nonces absents, intégrées directement à votre CI.

- Auteur : Clément Hadrot
- Publié le : 2024-04-01
- Mis à jour le : 2024-04-01
- Catégorie : Tests
- URL : https://wpmoderne.dev.wordpress-developpement.fr/tests/semgrep-analyse-securite-wordpress/

## L’essentiel

- Les règles Semgrep se lisent presque comme du code exemple
- Trois familles de failles couvrent la majorité des vulnérabilités WordPress
- L'intégration en CI bloque la merge avant la mise en production

Une extension cliente affichait, dans un tableau d'administration, une colonne construite avec `echo $_GET['tri']` sans le moindre échappement. Le code avait été relu par deux développeurs différents lors de la revue de pull request, et personne ne l'avait remarqué : c'est une ligne banale, noyée dans cinquante autres, et l'œil humain rate ce genre de détail bien plus souvent qu'on ne le pense. C'est exactement le type de faille qu'un outil d'analyse statique repère en une fraction de seconde, à condition qu'on lui ait appris à la reconnaître.

Semgrep s'est imposé ces dernières années comme l'outil d'analyse statique le plus accessible pour ce genre de contrôle : ses règles s'écrivent dans un format YAML lisible, proche du code qu'elles recherchent, sans nécessiter de maîtriser un langage de requête complexe comme pour d'autres moteurs d'analyse.

## Trois familles de failles à couvrir en priorité

Sur un projet WordPress, trois catégories de vulnérabilités concentrent l'essentiel du risque réel et se prêtent particulièrement bien à une détection automatisée fiable, avec peu de faux positifs si les règles sont bien écrites.

### Échappement de sortie manquant

Toute donnée affichée qui provient d'une entrée utilisateur, d'une méta ou d'une option, doit passer par une fonction d'échappement adaptée au contexte : `esc_html()`, `esc_attr()`, `esc_url()`. L'absence de ces fonctions autour d'un `echo` direct de superglobale est la faille la plus fréquente et la plus simple à détecter statiquement.

### Requêtes SQL non préparées

Toute concaténation directe d'une variable dans une requête passée à `$wpdb->query()` ou `$wpdb->get_results()`, sans passer par `$wpdb->prepare()`, ouvre la porte à une injection SQL.

### Nonces absents sur les actions sensibles

Un traitement de formulaire d'administration, ou un point d'entrée AJAX, sans vérification via `wp_verify_nonce()` ou `check_admin_referer()`, expose l'action à une falsification de requête intersite.

## Écrire une première règle : sortie non échappée

Une règle Semgrep se déclare dans un fichier YAML, avec un motif qui ressemble au code recherché et des méta-variables qui capturent les parties variables :

```
rules:
  - id: wp-echo-superglobale-non-echappee
    languages: [php]
    severity: ERROR
    message: >
      Sortie directe d'une superglobale sans fonction d'échappement.
      Utilisez esc_html(), esc_attr() ou esc_url() selon le contexte.
    patterns:
      - pattern-either:
          - pattern: echo $_GET[$KEY];
          - pattern: echo $_POST[$KEY];
          - pattern: echo $_REQUEST[$KEY];
```

Cette règle, une fois exécutée, a immédiatement remonté onze occurrences sur le dépôt cité en introduction, dont la ligne fautive de départ et deux variantes similaires que personne n'avait signalées.

> L'essentiel à retenir : Les règles Semgrep se lisent presque comme du code exemple ; Trois familles de failles couvrent la majorité des vulnérabilités WordPress ; L'intégration en CI bloque la merge avant la mise en production

## Traquer les requêtes non préparées

La règle suivante cible les appels à `$wpdb` avec une chaîne interpolée directement, plutôt qu'un appel à `prepare()` :

```
rules:
  - id: wp-wpdb-requete-non-preparee
    languages: [php]
    severity: ERROR
    message: >
      Requête $wpdb construite par interpolation de variable.
      Utilisez $wpdb->prepare() avec des espaces réservés.
    patterns:
      - pattern: $WPDB->query("... $VAR ...")
      - metavariable-regex:
          metavariable: $WPDB
          regex: ^\$wpdb$
```

Cette règle attrape la majorité des cas réels, mais laisse volontairement de côté les constructions de requête réparties sur plusieurs variables concaténées avant l'appel, un motif plus difficile à capturer sans générer de faux positifs sur du code legacy complexe. Sur ces cas-là, la revue humaine reste nécessaire.

## Détecter l'absence de vérification de nonce

Ici, l'exercice est différent : il ne s'agit pas de repérer un motif dangereux, mais l'absence d'un motif protecteur dans une fonction qui traite une action sensible, ce qui demande une règle un peu plus élaborée s'appuyant sur le contexte de la fonction englobante plutôt qu'une seule ligne isolée. Une approche pragmatique consiste à cibler spécifiquement les fonctions accrochées aux hooks `wp_ajax_` et à vérifier qu'elles contiennent bien un appel à `check_ajax_referer` ou `wp_verify_nonce` dans leur corps, via une règle utilisant `pattern-not-inside` pour exclure les cas déjà couverts.

## Intégrer Semgrep à la chaîne d'intégration continue

Une fois les règles maison écrites et validées sur le code existant, on les regroupe dans un fichier de configuration versionné avec le projet, et on ajoute une étape dédiée dans le pipeline, indépendante des tests PHPUnit :

```
semgrep-securite:
  stage: analyse
  image: returntocorp/semgrep
  script:
    - semgrep --config .semgrep/regles-wordpress.yml --error
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
```

Le drapeau `--error` fait échouer la commande, donc le job de CI, dès qu'une règle de sévérité `ERROR` est déclenchée, ce qui empêche mécaniquement la fusion tant que la faille n'est pas corrigée ou explicitement justifiée dans le code via un commentaire d'exclusion documenté.

> Une règle d'analyse statique qui ne bloque rien reste une simple suggestion que l'équipe finira par ignorer sous la pression des délais. C'est le blocage effectif du build qui change le comportement, pas la présence du rapport.

## Limites à connaître avant de s'y fier aveuglément

Semgrep analyse le code de façon syntaxique, sans exécuter réellement le programme : il peut manquer des vulnérabilités qui transitent par plusieurs fonctions intermédiaires ou par des mécanismes dynamiques comme `call_user_func()` avec un nom de fonction construit dynamiquement. Il complète une revue de sécurité humaine et un audit des dépendances tierces, il ne les remplace pas.

## En résumé

Trois familles de règles bien écrites, échappement de sortie, requêtes préparées et vérification de nonce, couvrent l'essentiel des vulnérabilités les plus fréquentes sur un code WordPress maison, avec un taux de faux positifs faible quand les motifs sont assez précis. Le vrai levier n'est pas l'outil en lui-même mais son intégration bloquante en CI : c'est elle qui transforme une bonne pratique de sécurité théorique en garde-fou effectif avant chaque mise en production.
