Comment puis-je m'assurer en toute sécurité qu'une variable ne contient qu'un nom de fichier valide?

8

Compte tenu du script ci-dessous, comment puis-je m'assurer que l'argument contient uniquement un nom de fichier valide à l'intérieur /home/charlesingalls/et non un chemin ( ../home/carolineingalls/) ou un caractère générique, etc.?

Je veux seulement que le script puisse supprimer un seul fichier du répertoire codé en dur donné. Ce script s'exécutera en tant qu'utilisateur privilégié.

#!/bin/bash

rm -f /home/charlesingalls/"$1"
Aaron Cicali
la source
2
Si vous ne voulez pas supporter "foo / bar", vérifiez simplement qu'il ne contient pas /. Les caractères génériques ne sont pas interprétés entre guillemets.
Random832
3
Si vous supprimez un fichier , ne l'utilisez pas -ravec rm. rm -rsert à la suppression récursive d'un répertoire et de tous les fichiers et répertoires situés en dessous. Il n'est utile que lors de la suppression de répertoires. Plus généralement, ne faites pas de fret culte. c'est-à-dire ne copiez pas simplement des choses qui semblent utiles dans votre ligne de commande ou votre script sans comprendre ce qu'elles font ou comment elles fonctionnent. Les dieux de l'avion qui apportent la cargaison magique peuvent se mettre en colère et supprimer tous vos fichiers.
cas
Bon point concernant le drapeau -r - je comprends son utilisation, mais je ne pensais pas clairement.
Aaron Cicali

Réponses:

7

Si vous voulez seulement supprimer un fichier dans /home/charlesingalls(et non un fichier dans un sous-répertoire), c'est facile: vérifiez simplement que l'argument ne contient pas de /.

case "$1" in
  */*) echo 1>&2 "Refusing to remove a file in another directory"; exit 2;;
  *) rm -f /home/charlesingalls/"$1";;
esac

Cela s'exécute rmmême si l'argument est .ou ..ou vide, mais dans ce cas, il rmne parviendra pas à supprimer un répertoire sans danger.

Les caractères génériques ne sont pas pertinents ici car aucune expansion de caractères génériques n'est effectuée.

Ceci est sûr même en présence de liens symboliques: si le fichier est un lien symbolique, le lien symbolique (qui est dedans /home/charlesingalls) est supprimé et la cible de ce lien n'est pas affectée.

Notez que cela suppose qu'il /home/charlesingallsne peut pas être déplacé ou modifié. Cela devrait être correct si le répertoire est codé en dur dans le script, mais s'il est déterminé à partir de variables, la détermination peut ne plus être valide au moment où la rmcommande s'exécute.

Sur la base des informations supplémentaires selon lesquelles l'argument est un nom d'hôte virtuel, vous devriez faire une liste blanche plutôt qu'une liste noire: vérifiez que le nom est un nom d'hôte virtuel raisonnable, plutôt que d'interdire simplement les barres obliques. Je vérifie que le nom commence par une lettre ou un chiffre minuscule et qu'il ne contient pas de caractères autres que des lettres minuscules, des chiffres, des points et des tirets.

LC_CTYPE=C LC_COLLATE=C
case "$1" in
  *[!-.0-9a-z]*|[!0-9a-z]*) echo >&2 "Invalid host name"; exit 2;;
  *) rm -f /home/charlesingalls/"$1";;
esac
Gilles 'SO- arrête d'être méchant'
la source
Dans ce cas, les fichiers sont des fichiers de configuration de serveur Web qui ont été générés par un processus précédent. Ils ont tous le même niveau d'importance (chacun constitue un hôte virtuel). Cependant, le répertoire dans lequel ils existent est adjacent à des répertoires similaires, et ils existent tous sous un répertoire qui appartient au serveur Web. Ce script particulier est destiné à permettre la suppression des configurations d'hôte virtuel sous un certain répertoire uniquement.
Aaron Cicali
cette approche a-t-elle un sens pour vous à condition que ce cas d'utilisation? J'apprécie votre expérience et admettrai qu'il y a certainement des moments où j'ai "amené un bazooka à une fusillade" pour automatiser quelque chose sous Linux.
Aaron Cicali
@AaronCicali Pourquoi le script peut-il supprimer l'une des configurations d'hôte et pas seulement celle qui appartient à l'entité qui a fait la demande d'origine? Pourquoi le nom d'hôte virtuel n'a-t-il pas été validé en premier (il ne contiendrait alors aucun caractère spécial)?
Gilles 'SO- arrête d'être méchant'
L'entité qui fait la demande d'origine est une interface graphique qui a en effet l'autorisation de supprimer l'un des hôtes virtuels de ce dossier. Le nom d'hôte virtuel est validé en premier. Il provient d'une liste d'hôtes virtuels précédemment créés (noms de domaine). Je crois que c'est le travail de ce script de s'assurer qu'il est aussi sécurisé qu'il peut l'être sans compter sur la sécurité d'une autre partie de l'application. À savoir, qu'il ne peut supprimer que des fichiers de ce répertoire spécifique. Il devra également effectuer des travaux de nettoyage supplémentaires.
Aaron Cicali
@AaronCicali Ok, comme vérification d'esprit supplémentaire, cela a du sens. Dans ce cas, vous devez mettre sur liste blanche: n'acceptez que les noms qui ressemblent à des noms d'hôtes plausibles. Si vous n'autorisez pas les sous-domaines, vous pourriez même interdire.
Gilles 'SO- arrête d'être méchant'
10

Cette réponse suppose qu'il $1est autorisé d'inclure des sous-répertoires. Si vous êtes intéressé par le cas le plus simple où $1devrait être un simple nom de répertoire, consultez l'une des autres réponses.


Les caractères génériques ne sont pas développés lorsqu'ils sont entre guillemets. Comme $1c'est entre guillemets, les caractères génériques ne sont pas un problème.

Les deux ../et les liens symboliques peuvent masquer l' emplacement réel d'un fichier. Ci-dessous sont des tests pour déterminer si le fichier est vraiment, pas seulement en apparence, sous le chemin que nous voulons.

Systèmes plus récents: utilisation realpath

Quant à savoir si le fichier est vraiment /home/charlesingalls/ou non, vous pouvez utiliser realpath:

realpath --relative-base=/home/charlesingalls/ "/home/charlesingalls/$1"  | grep -q '^/' && exit 1

Ce qui précède s'exécute exit 1si le fichier spécifié par se $1trouve ailleurs que sous le répertoire /home/charlesingalls/. realpathcanonise l'ensemble du chemin, en éliminant les liens symboliques et ../.

realpath fait partie de GNU coreutils et devrait être disponible sur n'importe quel système Linux.

realpathnécessite GNU coreutils 8.15 (janvier 2012) ou mieux .

Exemples

Pour montrer comment realpath suit ../pour déterminer l'emplacement réel d'un fichier (par exemple, l' -qoption grep est omise pour que la sortie réelle de grep soit visible):

$ touch /tmp/test
$ realpath --relative-base=$HOME "$HOME/../../tmp/test" | grep '^/' && echo FAIL
/tmp/test
FAIL

Pour montrer comment il suit les liens symboliques:

$ ln -s /tmp/test ~/test
$ realpath --relative-base=$HOME "$HOME/test" | grep '^/' && echo FAIL
/tmp/test
FAIL

Systèmes plus anciens: utilisation readlink -e

readlinkest également capable de cononicaliser un chemin, en suivant à la fois les liens symboliques et ../:

readlink -e "$HOME/test" | grep -q "^$HOME" || exit 1

En utilisant les mêmes fichiers d'exemple:

$ readlink -e "$HOME/../../tmp/test" | grep "$HOME" || echo FAIL
FAIL
$ readlink -e "$HOME/test" | grep "^$HOME" || echo FAIL
FAIL

En plus d'être disponibles sur les anciens systèmes GNU, des versions de readlinksont disponibles sur BSD.

John1024
la source
Mon Ubuntu 14.04 coreutilsn'a pasrealpath
heemayl
1
Cela semble être plus de ce que je recherche, mais malheureusement mon serveur Centos 6.5 n'a pas de chemin réel. Je ne suis pas en mesure de l'installer. Google recherche des mentions de "readlink -f" remplaçant realpath, mais je ne l'ai pas encore fait fonctionner.
Aaron Cicali
1
Le sous-titre mentionne -f("tous les composants sauf le dernier doivent exister") et les exemples utilisent -e("tous les composants doivent exister"), ce qui est un peu déroutant.
isanae
2
@AaronCicali Ce n'est certainement pas la réponse dont vous avez besoin, car c'est dangereusement faux. Vous ne devez pas résoudre les liens symboliques . La cible du lien symbolique peut varier entre le moment où vous le vérifiez et le moment où vous l'utilisez. (C'est une catégorie bien connue d'erreurs de conception ). De plus, cela n'a aucun sens de résoudre les liens symboliques car il rmagit sur l'argument lui-même, pas sur sa cible.
Gilles 'SO- arrête d'être méchant'
1
Astuce: à utiliser grep -qpour éviter grepde produire des lignes correspondantes. Vous obtenez toujours le statut de sortie &&et ||travaillez toujours exactement comme vous en avez l'habitude.
un CVn du
2

Si vous voulez interdire complètement les chemins, le moyen le plus simple est de tester si la variable contient une barre oblique ( /). En bash:

if [[ "$1" = */* ]] ; then...

Cela bloquera tous les chemins, y compris foo/bar. Vous pouvez tester à la ..place, mais cela laisserait la possibilité de liens symboliques pointant vers des répertoires en dehors du chemin cible.

Si vous souhaitez uniquement autoriser la suppression d'un seul fichier, je ne pense pas que vous devriez utiliser rm -r.


En outre, selon ce que vous faites, vous pouvez utiliser les autorisations de fichiers du système pour autoriser uniquement la suppression des fichiers que l'utilisateur peut supprimer lui-même. Quelque chose comme ça:

su charlesingalls -c "rm /home/charlesingalls/'$1'"

Bien que @Gilles ait commenté, cela a un problème de citation: il échouera s'il $1contient une seule citation, donc la variable doit être testée pour cela en premier (avec par exemple if [[ "$1" = *\'* ]] ; then fail...ou plutôt en mettant en liste blanche un jeu de caractères sensible), ou le nom de fichier transmis une variable d'environnement avec par exemple

file="$1" su charlesingalls -c 'rm "/home/charlesingalls/$file"'
ilkkachu
la source
Bon point concernant le drapeau -r, cela ne devrait vraiment pas être là. Suppression maintenant ...
Aaron Cicali
Notez que votre sucommande est interrompue car la citation est incorrecte. Vous exécutez une commande avec l'argument interpolé comme un extrait de shell. Par exemple, si l'argument est $(touch foo)alors votre code s'exécute touch foo.
Gilles 'SO- arrête d'être méchant'
@ Gilles, merde, je savais qu'il devait y avoir quelque chose de mal. la citation imbriquée n'est pas mon amie préférée. Je pense que la version actuelle fonctionne mieux, les guillemets doubles évalueront la variable dans le shell externe et les guillemets simples l'empêcheront d'être évaluée dans le shell interne. Aucune garantie.
ilkkachu
@ilkkachu Cette version échoue si l'argument contient une seule citation. (Mais seulement dans ce cas, ce qui rend la validation beaucoup plus facile que lors de l'utilisation de guillemets doubles.) Il est impossible d'interpoler directement une chaîne arbitraire. Vous devez soit masser la chaîne (par exemple, remplacer tout 'par '\''), soit la passer par un autre canal tel qu'une variable d'environnement (ce que je ferais ici:) file_to_remove="$1" su -c 'rm "/home/charlesingalls/$file_to_remove"'. Il s'avère que le nom de fichier est censé être un nom d'hôte virtuel, donc rejeter tous les caractères spéciaux serait également correct ici.
Gilles 'SO- arrête d'être méchant'