Introduction
Bash propulse des étapes de build, des déploiements, des traitements de données et des opérations du quotidien. Cette portée en fait aussi une source fréquente d’incidents, de fuites de données et de mésusages de privilèges. Ce guide se concentre sur des techniques pratiques et copiables pour les administrateurs Linux, SRE et développeurs qui maintiennent des scripts Bash opérationnels. Il fixe aussi des limites claires : Bash n’est pas une barrière de sécurité. Si vous avez besoin d’analyse complexe, de cryptographie à haut risque ou d’exécution multi‑locataire non fiable, privilégiez un langage plus sûr (Python, Go, Rust) ou un outil dédié.
Modèle de menaces en une page
Supposez que votre script puisse faire face aux risques suivants :
- Entrées non fiables et injection de commandes
- Surprises dues au découpage de mots et à l’expansion des jokers (glob)
- Environnement hostile et manipulation de PATH
- Conditions de course et fichiers temporaires non sûrs
- Divulgation de secrets via argv, les journaux ou l’historique
- Privilèges excessifs ou mésusage de setuid
- Compromission de dépendances et téléchargements non vérifiés
- Différences de locale qui modifient le comportement d’analyse
Faites correspondre les risques à des contrôles concrets et à des preuves observables que vous pouvez vérifier :
| Menace | Contrôle principal | Preuve vérifiable |
|---|---|---|
| Injection de commandes à partir d’entrées non fiables | Citer toutes les expansions, utiliser des tableaux pour argv, listes d’autorisation (allowlists) pour les actions, ne jamais utiliser eval | Des tests unitaires avec des entrées hostiles n’exécutent aucune commande inattendue |
| Découpage de mots et expansion des jokers (glob) | Citer les variables, utiliser des tableaux, traiter les données utilisateur comme des données et non comme de la syntaxe shell | Des tests avec espaces, retours à la ligne et caractères jokers se comportent de façon prévisible |
| Environnement hostile et manipulation de PATH | Définir un PATH sûr, utiliser des chemins absolus quand approprié, désactiver les variables risquées | Le script affiche le PATH effectif en mode débogage, binaires résolus via command -v ou chemins absolus |
| Fichiers temporaires non sûrs et conditions de course | Utiliser mktemp, umask 077, vérifier la propriété, restreindre les répertoires, éviter les hypothèses TOCTOU | ls -ld des répertoires temporaires montre le mode 700 et l’appartenance à l’utilisateur; les tests de liens symboliques échouent de façon sûre |
| Divulgation de secrets | Ne pas passer de secrets dans argv, éviter set -x, caviarder les journaux, préférer des fichiers protégés ou des descripteurs de fichier | La sortie de ps ne montre aucun secret; les journaux affichent REDACTED le cas échéant |
| Privilèges excessifs | Exécuter sous un compte non privilégié, restreindre finement les règles sudo, séparer les étapes privilégiées | id affiche un uid non-root; l’entrée sudoers est minimale et auditée |
| Téléchargements non vérifiés et compromission de dépendances | Vérification HTTPS, sources épinglées, contrôle d’empreinte (checksum) ou de signature, délais limités | Validation d’empreinte requise avant exécution; test négatif bloque en cas d’écart |
| Analyse sensible à la locale | Définir LC_ALL=C pour un comportement déterministe d’outils comme sort, awk, grep | Les tests réussissent sous différents réglages de LANG |
Expansions plus sûres et gestion des arguments
Traitez toute donnée contrôlée par l’utilisateur strictement comme des données. Règles courantes :
- Toujours citer les expansions de variables : "$var" et non $var.
- Utiliser des tableaux pour construire argv et préserver les limites d’arguments : cmd "${args[@]}".
- Utiliser -- pour terminer l’analyse des options quand vous transmettez des données utilisateur à des commandes qui l’acceptent.
- Valider tôt les formats attendus (p. ex. chemins, noms d’hôte, plages numériques) et rejeter tout le reste.
- Ne pas construire des commandes sous forme de chaînes.
Exemples :
# Préserver correctement les frontières d’arguments avec des tableaux
args=(--name "$NAME" --path "$P")
mytool "${args[@]}" --
# Itération sûre sur des noms de fichiers
while IFS= read -r -d '' file; do
printf '%s\n' "$file"
done < <(find "$SOURCE" -type f -print0)
Constructions dangereuses à éviter (et quoi faire à la place)
- eval et bash -c avec des entrées interpolées : tous deux interprètent du texte comme du code shell, ouvrant la voie à l’injection. À la place, assemblez argv avec des tableaux et exécutez directement.
- Expansion indirecte comme ${!name} : facile à mal utiliser et peut traverser des frontières de variables. Préférez des mappages explicites ou des instructions case.
- Téléchargement de scripts distants et exécution immédiate (curl | bash) : téléchargez sur disque, vérifiez l’authenticité et l’intégrité avec des empreintes ou des signatures obtenues par un canal indépendant, inspectez, puis exécutez.
Des alternatives plus sûres figurent dans le tableau comparatif ci‑dessous.
Mode strict et écueils du monde réel
set -euo pipefail est une valeur par défaut solide, mais ce n’est pas une solution complète.
- -e (errexit) arrête le script quand une commande échoue, mais pas dans tous les contextes. Il est ignoré dans les listes conditionnelles comme cmd || recours et cmd && suivant, sauf si la commande en échec est la dernière. Dans les pipelines, seule la dernière commande est prise en compte à moins d’activer pipefail.
- Les échecs dans les substitutions de commande peuvent être masqués selon le contexte. Lors de la capture de sortie, vérifiez explicitement le statut de sortie si la justesse est critique.
- -u (nounset) traite les variables non définies comme des erreurs. Utilisez ${var:-default} pour des valeurs par défaut intentionnelles.
- pipefail fait échouer un pipeline si l’une des commandes échoue.
- Utilisez set -E pour que les pièges ERR se déclenchent dans les fonctions et les sous‑shells.
Exemples de motifs :
set -Eeuo pipefail
trap 'printf "ERREUR à la ligne %s\n" "$LINENO" >&2' ERR
# Si vous souhaitez ignorer un échec, faites-le explicitement et documentez-le
may_fail || printf 'non fatal : may_fail a échoué\n' >&2
# Toujours vérifier les substitutions critiques
data=$(curl --fail --silent --show-error --max-time 10 "$url") || {
printf 'échec du téléchargement\n' >&2; exit 1; }
Ne prétendez pas que le mode strict rend un script sûr. Il rend simplement les échecs plus visibles.
Validation des entrées et noms de fichiers sûrs
- Utiliser des listes d’autorisation et des instructions case explicites pour les actions.
- Valider les chemins de fichiers avec -e ou -d et rejeter les chemins vides ou proches de la racine avant les opérations destructrices.
- Traiter les données utilisateur comme des octets opaques; ne pas s’appuyer sur le découpage de mots. Utiliser read -r et find -print0 pour des noms de fichiers arbitraires.
- Terminer les options par -- avant les données positionnelles quand c’est pris en charge.
action=${1:-}
case "$action" in
backup|verify) ;;
*) printf 'Action invalide : %s\n' "$action" >&2; exit 2 ;;
esac
Exécution prévisible : PATH, environnement, locale
- Définir un PATH minimal et réputé sûr puis l’exporter.
- Utiliser des chemins absolus pour les outils critiques ou les résoudre une fois via command -v.
- Désactiver les variables risquées dont vous n’avez pas besoin : IFS, CDPATH, GLOBIGNORE.
- Définir LC_ALL=C pour un traitement de texte déterministe.
PATH="/usr/sbin:/usr/bin:/sbin:/bin"; export PATH
unset CDPATH GLOBIGNORE
export LC_ALL=C LANG=C
for bin in tar sha256sum find; do command -v "$bin" >/dev/null || exit 127; done
Ressources temporaires et sécurité face aux conditions de course
- Créer des fichiers et répertoires uniques avec mktemp et un umask restrictif (077).
- Vérifier la propriété et les permissions si vous opérez dans des répertoires partagés.
- Utiliser des pièges (trap) pour nettoyer à la sortie, et éviter les hypothèses TOCTOU sur des fichiers que vous n’avez pas créés à l’instant.
umask 077
tmpdir=$(mktemp -d)
cleanup() { rm -rf -- "$tmpdir"; }
trap cleanup EXIT
Gestion sûre des secrets
- Ne jamais passer de secrets dans argv ni les consigner. De nombreux systèmes exposent argv via les listes de processus.
- Préférer la lecture depuis stdin, des fichiers protégés (chmod 600) ou un gestionnaire de secrets.
- Ne pas activer set -x autour des secrets; si vous devez tracer, caviardez avant d’afficher.
read -r -s -p 'Entrez le jeton API : ' API_TOKEN; printf '\n' >&2
hdr=$(mktemp "$tmpdir/h.XXXXXX"); chmod 600 "$hdr"
printf 'Authorization: Bearer %s\n' "$API_TOKEN" > "$hdr"
# Utiliser @fichier pour garder les secrets hors d'argv
curl --fail --silent --show-error --header @"$hdr" "$endpoint" > "$tmpdir/resp.json"
shred -u "$hdr" 2>/dev/null || rm -f -- "$hdr"
Privilèges
- Faire tourner les scripts par défaut sous un compte non privilégié.
- Éviter les scripts shell setuid; ils sont fondamentalement non sûrs.
- Utiliser des règles sudo à portée très limitée et séparer les opérations privilégiées dans un petit utilitaire passé en revue.
- Valider la propriété et les permissions des fichiers avant d’opérer dessus.
Téléchargements et appels réseau
- Exiger HTTPS avec TLS moderne, épingler les sources et vérifier l’intégrité avec des empreintes ou des signatures issues d’un canal indépendant.
- Utiliser des délais bornés et des reprises, et échouer de façon sûre en cas d’erreur de vérification.
curl --proto '=https' --tlsv1.2 --location --fail --silent --show-error \
--output "$tmpdir/tool.sh" "$url"
actual=$(sha256sum "$tmpdir/tool.sh" | awk '{print $1}')
[ "$actual" = "$expected" ] || { printf 'empreinte (checksum) non concordante\n' >&2; exit 1; }
Modèles Bash risqués versus plus sûrs
| Modèle risqué | Modèle plus sûr | Mise en garde |
|---|---|---|
| Construire une commande en chaîne avec des variables non citées | Construire argv avec des tableaux puis exécuter : cmd "${args[@]}" | Les tableaux préservent les limites d’arguments |
| Variable non citée dans une commande | Citer : rm -- "$target" | Empêche le découpage et le globbing |
| eval sur des chaînes construites | Ne pas utiliser eval; appeler la commande directement avec des tableaux | Élimine l’interprétation des données comme du code |
| bash -c avec des entrées interpolées | Exécuter directement le binaire cible avec des arguments validés | Évite l’analyse en sous‑shell de données utilisateur |
| Expansion indirecte ${!name} | Utiliser une instruction case ou un tableau associatif | Garde le contrôle sur les variables lues |
| for x in $(cat file) | while IFS= read -r line; do ...; done < file | Préserve les espaces et caractères spéciaux |
| Fichier temporaire nommé à la main dans /tmp | tmp=$(mktemp); chmod 600 "$tmp" | Évite collisions et courses |
| Téléchargement directement pipé dans un shell | Télécharger sur disque, vérifier empreinte ou signature, puis exécuter | Nécessite un canal de confiance indépendant |
Validation statique et dynamique
- Vérification de syntaxe : bash -n yourscript.sh
- Linting : ShellCheck pour détecter les problèmes de citation, de découpage et de tableaux.
- Tests unitaires et d’intégration : inclure des tests négatifs avec des entrées hostiles et des noms de fichiers atypiques.
- Garde‑fous CI : exécuter syntaxe, lint et tests à chaque changement; exiger une revue de code.
- Journalisation : rester observable sans fuite; caviarder les secrets et éviter le traçage autour des identifiants.
Exemple pratique : sauvegarde sécurisée et testable
Scénario illustratif : créer une archive tar d’un répertoire source vers un répertoire de destination, avec sortie d’empreinte. Tout le travail se fait dans un espace temporaire isolé.
#!/usr/bin/env bash
set -Eeuo pipefail
set -o noclobber
umask 077
export LC_ALL=C LANG=C
PATH="/usr/sbin:/usr/bin:/sbin:/bin"; export PATH
trap 'printf "ERREUR à la ligne %s\n" "$LINENO" >&2' ERR
usage() {
printf 'Utilisation : %s --action backup --source DIR --dest DIR\n' "$0"
}
# Analyse des drapeaux
ACTION=""; SOURCE=""; DEST=""
while [ $# -gt 0 ]; do
case "$1" in
--action) ACTION=${2:-}; shift 2 ;;
--source) SOURCE=${2:-}; shift 2 ;;
--dest) DEST=${2:-}; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) printf 'Option inconnue : %s\n' "$1" >&2; usage; exit 2 ;;
esac
done
case "$ACTION" in
backup) ;;
*) usage; exit 2 ;;
esac
# Validation des chemins
[ -d "$SOURCE" ] || { printf 'La source doit être un répertoire\n' >&2; exit 2; }
[ -n "$DEST" ] || { printf 'La destination doit être fournie\n' >&2; exit 2; }
[ -d "$DEST" ] || { printf 'La destination doit être un répertoire existant\n' >&2; exit 2; }
# Résolution des outils requis
for bin in tar sha256sum find; do command -v "$bin" >/dev/null || { printf 'Binaire manquant : %s\n' "$bin" >&2; exit 127; }; done
# Espace de travail isolé
workdir=$(mktemp -d)
cleanup() { rm -rf -- "$workdir"; }
trap cleanup EXIT
# Créer l'archive dans l'espace de travail, puis la déplacer en place
archive_name="backup-$(date +%Y%m%d-%H%M%S).tar"
archive_path="$workdir/$archive_name"
(
cd "$SOURCE"
# Utiliser tar avec un chemin déterministe
tar -cf "$archive_path" .
)
# Calculer l'empreinte
sum=$(sha256sum "$archive_path" | awk '{print $1}')
# Déplacer les résultats de façon atomique dans la destination
final_archive="$DEST/$archive_name"
final_checksum="$DEST/$archive_name.sha256"
# S'assurer de ne pas écraser un fichier existant involontairement
[ -e "$final_archive" ] && { printf 'Refus d\'écraser une archive existante\n' >&2; exit 1; }
[ -e "$final_checksum" ] && { printf 'Refus d\'écraser une empreinte existante\n' >&2; exit 1; }
mv -- "$archive_path" "$final_archive"
printf '%s %s\n' "$sum" "$archive_name" > "$final_checksum"
chmod 600 "$final_checksum"
printf 'Créés %s et %s\n' "$final_archive" "$final_checksum"
Validations et tests rapides exécutables en toute sécurité :
# Vérifications statiques
bash -n safe_backup.sh
shellcheck -s bash safe_backup.sh
# Préparer un jeu d\'essai isolé avec des noms de fichiers piégeux
src=$(mktemp -d); dest=$(mktemp -d)
mkdir -p "$src/sub dir"; touch "$src/sub dir/file [1].txt" "$src/asterisk*.txt"
# Lancer la sauvegarde
./safe_backup.sh --action backup --source "$src" --dest "$dest"
# Vérifier que l'archive existe et que l'empreinte concorde
sha256sum -c "$dest"/*.sha256 --ignore-missing --strict
# Test négatif : tentative d\'écrasement
./safe_backup.sh --action backup --source "$src" --dest "$dest" || true
Liste de contrôle de durcissement et de tests avant déploiement
| Élément | Priorité | Responsable | Preuve |
|---|---|---|---|
| Shebang et options strictes set -Eeuo pipefail avec piège ERR | Élevée | En-tête présent et le piège affiche la ligne en cas d’échec | |
| Assainir PATH et l’environnement, définir LC_ALL=C | Élevée | PATH affiché en débogage, locale exportée | |
| Citer les expansions et utiliser des tableaux pour argv | Élevée | ShellCheck propre; tests avec espaces et jokers réussissent | |
| Pas de eval, backticks, ni bash -c avec entrée interpolée | Élevée | Élément de liste de revue; vérifications grep en CI | |
| Listes d’autorisation des entrées et validation explicite | Élevée | Instruction case pour les actions; entrées invalides rejetées | |
| Répertoires temporaires via mktemp et nettoyage par trap | Moyenne | mktemp utilisé dans le code; répertoires temporaires supprimés à la sortie | |
| Modèle de privilèges : non privilégié par défaut, sudo restreint | Élevée | id non-root; entrée sudoers passée en revue | |
| Secrets : pas de fuite argv, journaux caviardés, pas de set -x près des secrets | Élevée | ps sans secrets; journaux contiennent REDACTED | |
| Réseau : HTTPS uniquement, sources épinglées, checksum ou signature, délais | Élevée | Vérification d’empreinte requise avant exécution | |
| Analyse statique : bash -n et ShellCheck sans erreur | Moyenne | Artéfacts CI au vert | |
| Tests : unitaires, intégration et entrées négatives | Élevée | Suite de tests réussie en CI | |
| Revue de code effectuée avant la mise en production | Élevée | Lien PR avec approbations |
Quand remplacer Bash
Choisissez Python, Go, Rust ou un outil d’automatisation déclaratif quand :
- Vous devez traiter en sécurité des entrées non fiables ou multi‑locataires.
- La tâche exige une analyse complexe, des données structurées ou une concurrence robuste.
- La maintenabilité et la testabilité à long terme priment sur la rapidité d’un one‑liner.
- Vous avez besoin de cryptographie solide, d’analyse vérifiée ou d’un riche écosystème de bibliothèques.
Utilisez Bash pour de petits assemblages (glue) où les arguments, l’environnement et les effets de bord sont strictement contrôlés.
Conclusion
Durcir Bash, c’est adopter des valeurs par défaut disciplinées et une intention explicite : citer les expansions, valider les entrées avec des listes d’autorisation, éviter eval et bash -c, gérer PATH et l’environnement de manière prévisible, utiliser mktemp et des traps pour les ressources temporaires, garder les secrets hors d’argv et des journaux, minimiser les privilèges et vérifier les téléchargements avant usage. Associez ces pratiques à des contrôles statiques et à des tests négatifs. Quand le problème dépasse Bash, passez à un langage ou un outil plus sûr avant que le risque ne vous y oblige.