GitHub Actions cache-mode : la fin du cache poisoning silencieux

GitHub Actions cache-mode : la fin du cache poisoning silencieux — illustration Ctrl+Alt+Prod (cache-mode GitHub Actions)

Écrit par

dans

Le 26 juin 2026, GitHub a changé silencieusement le comportement du cache de tous les workflows déclenchés par un événement pull_request_target ou une cascade workflow_run venant d’un fork : ces jobs ne pouvaient plus écrire dans le cache, seulement le lire. Un correctif discret, mais un vrai correctif de sécurité — il bloque un scénario d’attaque documenté depuis des années : un contributeur externe pousse une pull request qui empoisonne le cache, et ton workflow push de confiance restaure ensuite cette entrée corrompue en pensant récupérer ses propres artefacts de build.

Depuis le 10 septembre 2026, cette protection binaire (lecture seule ou rien) devient un vrai réglage. cache-mode passe en disponibilité générale sur tous les plans github.com. Voici ce que ça change pour tes pipelines, et comment configurer le bon niveau d’accès sans casser tes builds.

Le problème du cache poisoning, en clair

Le cache GitHub Actions n’a jamais été pensé comme un espace de stockage fiable à 100 %. N’importe quel job avec un accès en écriture peut y déposer une entrée sous une clé qu’un autre job réutilisera plus tard — c’est justement tout l’intérêt du cache : accélérer les builds en réutilisant des node_modules, des dépendances Maven ou des couches Docker déjà téléchargées.

Le souci apparaît quand deux workflows différents partagent la même clé de cache sans partager le même niveau de confiance. Un pull_request_target tourne avec les secrets et permissions du dépôt de base, même quand il traite une PR venant d’un fork inconnu — c’est justement pour ça que ce trigger existe. Si ce job a le droit d’écrire dans le cache, un attaquant n’a besoin que d’une PR pour y glisser une dépendance modifiée. Ton workflow push suivant restaure cette entrée en toute confiance, et exécute le code injecté avec des privilèges bien supérieurs à ceux d’un simple contributeur externe.

Ce qui a changé le 26 juin, et ce qui change le 10 septembre

Première étape, en juin : GitHub a imposé des jetons en lecture seule pour les événements « non fiables » — pull_request_target, issue_comment, et les cascades workflow_run initiées depuis un fork, dès que le contexte pointe vers le SHA de la branche par défaut. Les événements considérés comme fiables (push, schedule, workflow_dispatch, repository_dispatch, delete, registry_package, page_build) gardaient l’accès complet. Une tentative d’écriture sur un job restreint ne fait pas échouer le job : elle génère juste un avertissement, et l’écriture est ignorée.

Cette bascule automatique protégeait déjà l’essentiel des cas. Mais elle ne laissait aucune marge de manœuvre. Impossible, par exemple, d’autoriser un job planifié à écrire dans le cache sans le lire (pour préchauffer un cache que d’autres jobs consommeront ensuite), ou de couper complètement l’accès cache d’un job qui n’en a pas besoin. cache-mode, en GA depuis le 10 septembre, comble ce vide avec quatre valeurs :

  • read — restaure les entrées existantes, ne peut rien sauvegarder. C’est le défaut appliqué automatiquement aux événements non fiables depuis juin.
  • write — lecture et écriture, le comportement classique. Défaut pour les événements fiables comme push.
  • write-only — sauvegarde sans jamais restaurer, utile pour un job dont le seul rôle est de préchauffer le cache pour les autres.
  • none — aucun accès au cache, ni en lecture ni en écriture.

Configurer cache-mode, au niveau workflow ou par job

Le champ se place soit en tête de fichier (avant jobs), soit directement dans la définition d’un job (après runs-on) :

name: Build et tests
on: [push, pull_request_target]

# Défaut pour tous les jobs du workflow
cache-mode: read

jobs:
  build:
    runs-on: ubuntu-latest
    # Ce job override le défaut du workflow
    cache-mode: write
    steps:
      - uses: actions/checkout@v6
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ hashFiles('package-lock.json') }}

La hiérarchie est simple à retenir : un réglage au niveau job écrase celui du workflow, et un workflow appelé (via workflow_call) ne peut jamais obtenir plus d’accès que celui accordé par l’appelant. Impossible donc pour un workflow réutilisable de s’octroyer discrètement un accès en écriture que le caller lui refuse.

La stratégie à adopter, sans y passer trois heures

Pas besoin de réécrire tous tes workflows dès aujourd’hui — les défauts de juin couvrent déjà le risque principal. Mais si tu veux verrouiller les choses proprement, la règle est simple : n’accorde jamais plus que ce dont un job a besoin, et pose-toi la question à chaque job qui touche à un pull_request_target.

Un job de build classique sur push reste en write, sans y toucher. Pour un job qui tourne sur pull_request_target mais qui n’a même pas besoin de lire le cache (un simple linter, par exemple), passe-le carrément en none — ça évite qu’il consomme des jetons de cache pour rien. Et si tu as un job planifié dédié à préchauffer le cache pour la nuit suivante, write-only est fait pour ce cas précis : il alimente le cache sans jamais avoir besoin de le lire lui-même.

Ce que je déconseille clairement : laisser write par défaut sur un workflow entier qui mélange des jobs push et des jobs déclenchés par PR externe, en comptant sur les défauts automatiques pour rattraper le coup à ta place. Ces défauts existent, mais un réglage explicite au niveau job documente ton intention — et survit à un futur changement de trigger que tu pourrais oublier de revérifier.

FAQ

Est-ce que cache-mode casse mes workflows existants ?

Non. Sans le champ, chaque job garde le comportement par défaut déjà en place depuis juin : lecture seule sur événements non fiables, lecture-écriture ailleurs. cache-mode est une option à ajouter volontairement, pas un changement de comportement forcé.

Ça marche aussi sur GitHub Enterprise Server ?

L’annonce du 10 septembre couvre github.com sur tous les plans. Si tu es sur GHES, vérifie la version de ta plateforme avant de t’appuyer sur ce champ — les fonctionnalités Actions arrivent en général avec un décalage sur les versions on-premise.

Une tentative d’écriture refusée fait-elle échouer le job ?

Non. Elle génère un avertissement dans les logs et l’écriture est silencieusement ignorée. Les restaurations en lecture, elles, ne sont jamais affectées par une restriction d’écriture.

Faut-il aussi revoir mes clés de cache existantes ?

Pas forcément, mais c’est le bon moment pour vérifier qu’elles n’incluent pas d’éléments contrôlables par un attaquant externe (nom de branche, titre de PR) sans validation préalable. cache-mode réduit le risque d’écriture non autorisée ; il ne remplace pas une clé de cache mal construite.

Passe à l’action

Si tes pipelines mélangent des jobs push et des jobs déclenchés par des PR externes — dépôt open source, contributions communautaires — c’est le moment de faire l’inventaire : quels jobs ont vraiment besoin d’écrire dans le cache, et lesquels peuvent basculer en read ou none. Une demi-heure de relecture de tes fichiers YAML vaut mieux qu’un cache empoisonné découvert en production.

Pour aller plus loin, la documentation officielle GitHub détaille la syntaxe complète des workflows, et le changelog GitHub revient sur le passage en GA de cache-mode ainsi que sur la restriction en lecture seule de juin. Si le sujet de la chaîne d’approvisionnement CI/CD t’intéresse, on avait détaillé les défenses anti-supply-chain côté GitHub Actions et npm dans un article précédent, et les leçons des attaques de mars dans celui-ci.

Commentaires

Une réponse à « GitHub Actions cache-mode : la fin du cache poisoning silencieux »

  1. […] sur du caching pour accélérer les builds, pense à vérifier en même temps ta configuration cache-mode — les deux réglages touchent au même menu Actions et autant les revoir ensemble. Et si tu […]

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *