Skip to content

fix(ci): sécurise le workflow de versionnage - #1

Merged
Deniz09OK merged 6 commits into
mainfrom
securite/versionning
Oct 10, 2026
Merged

Deniz09OK merged 6 commits into
mainfrom
securite/versionning

Conversation

@Deniz09OK

@Deniz09OK Deniz09OK commented Oct 10, 2026 •

Copy link
Copy Markdown
Owner

Contexte

Le workflow .github/workflows/versionning.yml (Semantic Versioning) présentait plusieurs défauts de sécurité et de robustesse. Cette PR les corrige, avec un commit par sujet. Les déclencheurs, le format semver, les règles de bump et le contenu du CHANGELOG ne changent pas.

Corrections

1. Injection de script (5ef4a09)

Le titre de PR, le login de l'auteur de la PR, le message, le nom et l'email du commit de tête, env.commits, env.user_name et l'entrée dry_run étaient interpolés via ${{ }} directement dans les blocs run:. Un message de commit du type fix: x"; <commande>; echo " exécutait la commande dans le job, avec le GITHUB_TOKEN.

  • Toutes ces valeurs passent désormais par le bloc env: du step et sont lues entre guillemets ("$COMMITS", "$USER_NAME"…).
  • Le calcul du bump analyse "$COMMITS" avec printf '%s\n' … | grep -E, sans interpolation.
  • Le eval de retry_push est remplacé par une exécution directe des arguments ("$@").
  • Les valeurs du contexte github utilisent les variables du runner ($GITHUB_EVENT_NAME, $GITHUB_REF_NAME, $GITHUB_SHA…).
  • Il ne reste plus aucun ${{ }} dans un bloc run:. Les expressions restantes se trouvent uniquement dans les if:, les env: et les outputs.

2. Injection dans GITHUB_ENV (f657926)

Ce point n'était pas dans la liste initiale, mais il est du même ordre. user_name, user_email et commit_message étaient écrits sous la forme nom=valeur, et commits avec le délimiteur fixe EOF. Un message de commit sur plusieurs lignes pouvait donc définir des variables pour tous les steps suivants, par exemple BASH_ENV=$(…), que bash évalue au démarrage de chaque step. Ces valeurs sont maintenant écrites avec nom<<délimiteur et un délimiteur aléatoire de 128 bits (openssl rand -hex 16). Il en va de même pour CHANGELOG.

3. Permissions minimales (5c49a5e)

  • Niveau workflow : contents: read.
  • Job versioning (le seul job, qui pousse le commit du changelog et le tag) : contents: write. Les permissions d'un job remplacent celles du workflow.
  • Aucun autre droit : le workflow n'appelle pas l'API et n'utilise que git push.

4. Boucle de push du changelog (136e28d)

La boucle réessayait le même git push. Si main avait avancé, les 3 tentatives étaient toutes rejetées (non fast-forward). Désormais :

  • git pull --rebase origin main avant chaque nouvelle tentative ;
  • en cas de conflit, git rebase --abort, puis la tentative suivante échoue normalement ;
  • le nombre d'essais reste borné par max_attempts (3), sans force push ;
  • l'attente inutile après le dernier essai est supprimée.

5. Tag déjà existant (4f4617f)

Quand le tag calculé existait sur un autre commit, l'ancien code exécutait 4 push : suppression distante, push du nouveau tag, nouvelle suppression via retry_push (qui effaçait le tag tout juste poussé), puis un nouveau push. Les deux opérations étaient donc en double, et le tag publié finissait déplacé sur HEAD.

Désormais, si le tag existe déjà, sa création est sautée et le tag n'est jamais modifié (ni git tag -d/-f, ni suppression distante, ni --force) :

  • tag déjà sur HEAD : rien à faire (comportement inchangé) ;
  • pas de bump (le tag calculé est le dernier tag) : log d'audit, le tag reste en place ;
  • tag existant sur un autre commit alors qu'un bump était attendu : ::warning:: dans le run, le tag n'est pas touché.

L'output new_version est renseigné dans tous les cas, comme avant. Le push utilise la référence explicite refs/tags/$TAG.

6. Action épinglée par SHA (2073407)

actions/checkout@v4 devient actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0.

  • Ce SHA a été vérifié contre le tag officiel v4.4.0 du dépôt actions/checkout (git ls-remote : tag léger, donc SHA de commit) et contre la page de la release v4.4.0.
  • C'est aussi le commit vers lequel pointe v4 aujourd'hui : le comportement est identique.
  • Le changement incompatible introduit en v4.4.0 (allow-unsafe-pr-checkout) ne concerne que pull_request_target et workflow_run, que ce workflow n'utilise pas.

Changement de comportement (nécessaire pour le point 5)

Avant, chaque push sur main sans bump (par exemple docs: ou chore:) déplaçait le dernier tag (ex. v0.8.0) sur le nouveau commit du changelog. Désormais, ce tag reste sur le commit où il a été publié. La section ## v0.8.0 du CHANGELOG continue d'agréger ces commits (logique inchangée), mais le tag ne les inclut plus. C'est la conséquence directe de « ne jamais déplacer un tag publié ». Incrémenter automatiquement aurait modifié les règles semver (un docs: aurait produit un patch). La création du tag est donc simplement sautée.

Validation

  • actionlint 1.7.7 avec shellcheck 0.11.0 : aucune erreur ni avertissement sur les 4 workflows. Sur main, actionlint signale l'injection via github.event.pull_request.title. Il ne reste que des info/style shellcheck déjà présents avant (>> $GITHUB_ENV sans guillemets).
  • Tests locaux : les steps sont extraits du YAML et exécutés avec bash -e comme sur le runner, sur main puis sur cette branche, avec un dépôt distant local. Résultats :
    • calcul du bump (feat, fix, point d'exclamation après le type, pied de page de rupture, emoji, docs seul) : versions identiques avant et après ;
    • message fix: x"; touch … : commande exécutée sur main, pas sur la branche ;
    • message multi-ligne avec BASH_ENV=$(…), et ligne EOF dans un commit : variable injectée sur main, pas sur la branche ;
    • push concurrent sur main : 3 échecs sur main ; rebase puis succès sur la branche ; conflit sur CHANGELOG.md : rebase annulé et échec propre, distant intact ;
    • tag : nouveau tag créé ; pas de bump ou collision : tag déplacé sur main, intact sur la branche ; course (tag publié après le checkout) : push refusé sans écrasement ;
    • job complet sur un push fix: : même version, même tag et CHANGELOG identique à main.

Hors périmètre (non modifié, à traiter séparément si besoin)

  • Pas de concurrency: : deux runs sur main peuvent encore se croiser (changelog et tag). Un groupe concurrency sans annulation sérialiserait les runs.
  • Le job tourne aussi sur pull_request avec contents: write. Pour une PR depuis un fork, le token reste en lecture seule ; pour une branche interne, il est en écriture, mais aucun step d'écriture ne s'exécute sur une PR. Séparer l'analyse et la publication en deux jobs permettrait de limiter l'écriture au seul job de publication.
  • $DRY_RUN (step Setup) n'est jamais défini : le message d'audit du mode dry-run ne s'affiche jamais.
  • L'entrée force_version n'est utilisée nulle part. En workflow_call, dry_run n'est pas lu (github.event.inputs au lieu de inputs).
  • Les autres workflows (ci-debian.yml, ci-freebsd.yml, ci-rocky.yml) utilisent encore des actions non épinglées par SHA.

Le titre de PR, le message et l'auteur du commit de tête, la liste des
commits depuis le dernier tag et l'entrée dry_run étaient interpolés
directement dans les scripts run. Un message de commit contenant un
guillemet suivi d'une commande suffisait à exécuter du code dans le job,
avec le GITHUB_TOKEN.

Ces valeurs passent désormais par le bloc env de chaque step et sont lues
entre guillemets dans le shell. Le calcul du bump (majeur, mineur, patch)
analyse la variable avec printf et grep, sans interpolation. Le eval de
retry_push est remplacé par une exécution directe des arguments. Les
valeurs du contexte github utilisent les variables GITHUB_* du runner.
…teur aléatoire

Le message de commit, l'auteur et la liste des commits étaient écrits dans
GITHUB_ENV sous la forme nom=valeur ou avec le délimiteur fixe EOF. Un
message multi-ligne pouvait donc définir d'autres variables pour tous les
steps suivants, par exemple BASH_ENV, que bash évalue au démarrage de
chaque step.

Ces valeurs sont maintenant écrites avec la syntaxe nom<<délimiteur et un
délimiteur aléatoire de 128 bits, impossible à prévoir dans un message.
Aucun bloc permissions n'était défini : le token recevait les droits par
défaut du dépôt, potentiellement en écriture sur tous les scopes.

Le workflow passe en contents: read par défaut. Le job versioning, qui
pousse le commit du changelog et le tag, reçoit uniquement contents:
write, les permissions d'un job remplaçant celles du workflow. Aucun autre
droit n'est accordé : le workflow n'utilise que git push.
La boucle de push réessayait le même git push sans récupérer la branche
distante. Si main avait avancé entre-temps, chaque tentative était rejetée
(non fast-forward) et le job échouait après trois essais.

Avant chaque nouvelle tentative, le commit du changelog est rejoué avec
git pull --rebase sur origin/main. En cas de conflit, le rebase est annulé
et la tentative suivante échoue normalement. Le nombre d'essais reste
borné par max_attempts, sans force push, et l'attente inutile après le
dernier essai est supprimée.
Quand le tag calculé existait déjà sur un autre commit, le step le
supprimait puis le recréait sur HEAD. Les push étaient en double :
suppression distante, push du nouveau tag, nouvelle suppression via
retry_push (qui effaçait le tag tout juste poussé), puis nouveau push.
Résultat : un tag publié était déplacé, notamment à chaque push sans
bump de version, où le dernier tag était avancé sur HEAD.

Désormais, si le tag existe déjà, sa création est sautée et il n'est
jamais modifié (pas de git tag -f ni de push --force). Un avertissement
est émis si le tag existe sur un autre commit alors qu'un bump était
attendu. Les push en double sont supprimés et le tag est poussé avec la
référence explicite refs/tags/.
Le tag mobile v4 peut être déplacé vers n'importe quel commit par le
dépôt de l'action. L'action est désormais référencée par le SHA complet
11d5960a326750d5838078e36cf38b85af677262, vérifié contre le tag de
release officiel v4.4.0 du dépôt actions/checkout. C'est le commit vers
lequel pointe v4 aujourd'hui : le comportement du checkout est inchangé.
Copilot AI balanced review requested due to automatic review settings October 10, 2026 11:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Deniz09OK Deniz09OK self-assigned this Oct 10, 2026
@Deniz09OK
Deniz09OK merged commit 2d57716 into main Oct 10, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants