chmod u+s mon-script.sh s’exécute sans erreur, ls -l affiche bien le bit s sur le fichier, et pourtant lancer le script ne l’exécute jamais avec les privilèges du propriétaire attendus. Ce n’est pas un bug de permissions mal comprises : le kernel Linux ignore délibérément le bit setuid sur tout script interprété, une protection de sécurité, pas un oubli.

Ce que setuid fait réellement sur un binaire

Sur un exécutable compilé, le bit setuid fait tourner le processus avec les privilèges du propriétaire du fichier plutôt que ceux de l’utilisateur qui le lance, l’exemple classique étant passwd (appartenant à root, permettant à n’importe quel utilisateur de modifier son propre mot de passe stocké dans un fichier que seul root peut écrire).

# Bit setuid visible : le "s" remplace le "x" du propriétaire
ls -l /usr/bin/passwd
# -rwsr-xr-x 1 root root ... /usr/bin/passwd

Pourquoi le kernel l’ignore sur un script

Un script commence toujours par une ligne shebang (#!/bin/bash) que le kernel interprète en lançant l’interpréteur désigné avec le script en argument. Entre le moment où le kernel ouvre le fichier setuid et celui où l’interpréteur l’exécute réellement, une fenêtre de temps existe qu’un attaquant peut exploiter (remplacer le script par un lien symbolique vers un autre fichier juste après l’ouverture, une classe de faille appelée race condition sur setuid scripts) : plutôt que de fermer cette fenêtre au cas par cas, le kernel Linux désactive purement et simplement setuid pour tout fichier commençant par un shebang, depuis des décennies.

# Le bit reste visible dans ls -l, mais le kernel
# l'ignore silencieusement à l'exécution : aucune erreur,
# le script tourne simplement avec les privilèges de l'appelant
chmod u+s mon-script.sh
./mon-script.sh

Le correctif : un wrapper compilé, pas le script lui-même

La solution standard consiste à écrire un petit wrapper en C, compilé en binaire, qui porte lui-même le bit setuid et se contente d’appeler le script cible : le kernel applique setuid normalement sur ce binaire compilé, sans la fenêtre de race condition propre aux fichiers interprétés. Cette indirection n’est pas une astuce de contournement, c’est le mécanisme prévu pour ce cas d’usage précis.

Le rapport avec sudo

Cette protection explique en partie pourquoi sudo reste la voie recommandée pour élever des privilèges de façon contrôlée plutôt que setuid sur un script : sudo est lui-même un binaire compilé avec setuid, conçu spécifiquement pour gérer cette élévation de façon auditée, avec un fichier sudoers qui définit précisément qui peut faire quoi, plutôt que de contourner la protection kernel avec un wrapper maison mal pensé.

À retenir

Le bit setuid reste visible dans ls -l sur un script, mais le kernel Linux l’ignore silencieusement à l’exécution, une protection délibérée contre une classe de race condition exploitable sur tout fichier interprété via shebang. Le correctif standard est un wrapper compilé en C portant le bit setuid, jamais le script lui-même. Cette même protection est une des raisons pour lesquelles sudo, lui-même un binaire setuid conçu pour cet usage précis, reste la voie recommandée pour toute élévation de privilège contrôlée plutôt qu’un contournement artisanal.