Pourquoi eval devrait-il être évité dans Bash, et que dois-je utiliser à la place?

107

Maintes et maintes fois, je vois des réponses de Bash sur Stack Overflow en utilisant evalet les réponses sont critiquées, jeu de mots destiné, pour l'utilisation d'une telle construction "diabolique". Pourquoi est-ce evalsi mauvais?

Si evalne peut pas être utilisé en toute sécurité, que dois-je utiliser à la place?

Zenexer
la source

Réponses:

148

Il y a plus à ce problème qu'il n'y paraît. Nous allons commencer par l'évidence: evala le potentiel d'exécuter des données "sales". Les données sales sont toutes les données qui n'ont pas été réécrites comme étant sûres pour une utilisation dans une situation XYZ; dans notre cas, il s'agit de toute chaîne qui n'a pas été formatée de manière à pouvoir être évaluée en toute sécurité.

La désinfection des données semble facile à première vue. En supposant que nous lançons une liste d'options, bash fournit déjà un excellent moyen de désinfecter des éléments individuels, et un autre moyen de nettoyer l'ensemble du tableau en une seule chaîne:

function println
{
    # Send each element as a separate argument, starting with the second element.
    # Arguments to printf:
    #   1 -> "$1\n"
    #   2 -> "$2"
    #   3 -> "$3"
    #   4 -> "$4"
    #   etc.

    printf "$1\n" "${@:2}"
}

function error
{
    # Send the first element as one argument, and the rest of the elements as a combined argument.
    # Arguments to println:
    #   1 -> '\e[31mError (%d): %s\e[m'
    #   2 -> "$1"
    #   3 -> "${*:2}"

    println '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit "$1"
}

# This...
error 1234 Something went wrong.
# And this...
error 1234 'Something went wrong.'
# Result in the same output (as long as $IFS has not been modified).

Disons maintenant que nous voulons ajouter une option pour rediriger la sortie en tant qu'argument vers println. Nous pourrions, bien sûr, simplement rediriger la sortie de println à chaque appel, mais à titre d'exemple, nous n'allons pas faire cela. Nous devrons utiliser eval, car les variables ne peuvent pas être utilisées pour rediriger la sortie.

function println
{
    eval printf "$2\n" "${@:3}" $1
}

function error
{
    println '>&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

Ça a l'air bien, non? Le problème est que eval analyse deux fois la ligne de commande (dans n'importe quel shell). Lors du premier passage d'analyse, une couche de cotation est supprimée. Avec les guillemets supprimés, certains contenus variables sont exécutés.

Nous pouvons résoudre ce problème en laissant l'expansion variable se produire dans le eval. Tout ce que nous avons à faire est de tout mettre entre guillemets simples, laissant les guillemets là où ils se trouvent. Une exception: nous devons étendre la redirection avant eval, donc cela doit rester en dehors des guillemets:

function println
{
    eval 'printf "$2\n" "${@:3}"' $1
}

function error
{
    println '&2' '\e[31mError (%d): %s\e[m' "$1" "${*:2}"
    exit $1
}

error 1234 Something went wrong.

Cela devrait fonctionner. Il est également sûr aussi longtemps que $1dans printlnn'est jamais sale.

Maintenant, attendez un instant: j'utilise la même syntaxe sans guillemets que nous avons utilisée à l'origine avec sudotout le temps! Pourquoi ça marche là-bas et pas ici? Pourquoi avons-nous dû tout citer? sudoest un peu plus moderne: il sait mettre entre guillemets chaque argument qu'il reçoit, bien que ce soit une simplification excessive. evalconcatène simplement tout.

Malheureusement, il n'y a pas de remplacement direct pour evalqui traite les arguments comme le sudofait, comme l' evalest un shell intégré; c'est important, car il prend l'environnement et la portée du code environnant lors de son exécution, plutôt que de créer une nouvelle pile et une nouvelle portée comme le fait une fonction.

Alternatives à eval

Les cas d'utilisation spécifiques ont souvent des alternatives viables à eval. Voici une liste pratique. commandreprésente ce à quoi vous enverriez normalement eval; substituez ce que vous voulez.

No-op

Un simple deux-points est un no-op dans bash:

:

Créer un sous-shell

( command )   # Standard notation

Exécuter la sortie d'une commande

Ne vous fiez jamais à une commande externe. Vous devez toujours contrôler la valeur de retour. Mettez-les sur leurs propres lignes:

$(command)   # Preferred
`command`    # Old: should be avoided, and often considered deprecated

# Nesting:
$(command1 "$(command2)")
`command "\`command\`"`  # Careful: \ only escapes $ and \ with old style, and
                         # special case \` results in nesting.

Redirection basée sur une variable

Dans le code d'appel, mappez &3(ou tout ce qui est supérieur à &2) à votre cible:

exec 3<&0         # Redirect from stdin
exec 3>&1         # Redirect to stdout
exec 3>&2         # Redirect to stderr
exec 3> /dev/null # Don't save output anywhere
exec 3> file.txt  # Redirect to file
exec 3> "$var"    # Redirect to file stored in $var--only works for files!
exec 3<&0 4>&1    # Input and output!

S'il s'agissait d'un appel unique, vous n'auriez pas à rediriger l'intégralité du shell:

func arg1 arg2 3>&2

Dans la fonction appelée, redirigez vers &3:

command <&3       # Redirect stdin
command >&3       # Redirect stdout
command 2>&3      # Redirect stderr
command &>&3      # Redirect stdout and stderr
command 2>&1 >&3  # idem, but for older bash versions
command >&3 2>&1  # Redirect stdout to &3, and stderr to stdout: order matters
command <&3 >&4   # Input and output!

Indirection variable

Scénario:

VAR='1 2 3'
REF=VAR

Mauvais:

eval "echo \"\$$REF\""

Pourquoi? Si REF contient un guillemet double, cela cassera et ouvrira le code aux exploits. Il est possible de désinfecter REF, mais c'est une perte de temps quand vous avez ceci:

echo "${!REF}"

C'est vrai, bash a une indirection variable intégrée à partir de la version 2. Cela devient un peu plus compliqué que evalsi vous voulez faire quelque chose de plus complexe:

# Add to scenario:
VAR_2='4 5 6'

# We could use:
local ref="${REF}_2"
echo "${!ref}"

# Versus the bash < 2 method, which might be simpler to those accustomed to eval:
eval "echo \"\$${REF}_2\""

Quoi qu'il en soit, la nouvelle méthode est plus intuitive, même si cela ne semble pas être le cas pour les programmeurs expérimentés qui en sont habitués eval.

Tableaux associatifs

Les tableaux associatifs sont implémentés intrinsèquement dans bash 4. Une mise en garde: ils doivent être créés avec declare.

declare -A VAR   # Local
declare -gA VAR  # Global

# Use spaces between parentheses and contents; I've heard reports of subtle bugs
# on some versions when they are omitted having to do with spaces in keys.
declare -A VAR=( ['']='a' [0]='1' ['duck']='quack' )

VAR+=( ['alpha']='beta' [2]=3 )  # Combine arrays

VAR['cow']='moo'  # Set a single element
unset VAR['cow']  # Unset a single element

unset VAR     # Unset an entire array
unset VAR[@]  # Unset an entire array
unset VAR[*]  # Unset each element with a key corresponding to a file in the
              # current directory; if * doesn't expand, unset the entire array

local KEYS=( "${!VAR[@]}" )  # Get all of the keys in VAR

Dans les anciennes versions de bash, vous pouvez utiliser l'indirection variable:

VAR=( )  # This will store our keys.

# Store a value with a simple key.
# You will need to declare it in a global scope to make it global prior to bash 4.
# In bash 4, use the -g option.
declare "VAR_$key"="$value"
VAR+="$key"
# Or, if your version is lacking +=
VAR=( "$VAR[@]" "$key" )

# Recover a simple value.
local var_key="VAR_$key"       # The name of the variable that holds the value
local var_value="${!var_key}"  # The actual value--requires bash 2
# For < bash 2, eval is required for this method.  Safe as long as $key is not dirty.
local var_value="`eval echo -n \"\$$var_value\""

# If you don't need to enumerate the indices quickly, and you're on bash 2+, this
# can be cut down to one line per operation:
declare "VAR_$key"="$value"                         # Store
echo "`var_key="VAR_$key" echo -n "${!var_key}"`"   # Retrieve

# If you're using more complex values, you'll need to hash your keys:
function mkkey
{
    local key="`mkpasswd -5R0 "$1" 00000000`"
    echo -n "${key##*$}"
}

local var_key="VAR_`mkkey "$key"`"
# ...
Zenexer
la source
4
Il me manque une mention de eval "export $var='$val'"... (?)
Zrin
1
@Zrin Il y a de fortes chances que cela ne fasse pas ce que vous attendez. export "$var"="$val"est probablement ce que vous voulez. Le seul moment où vous pourriez utiliser votre formulaire est si var='$var2', et vous voulez le double-déréférencer - mais vous ne devriez pas essayer de faire quelque chose comme ça dans bash. Si vous devez vraiment, vous pouvez utiliser export "${!var}"="$val".
Zenexer
1
@anishsane: Pour votre Suppose, x="echo hello world";puis pour exécuter tout ce qui est contenu dans x, nous pouvons utiliser eval $xCependant, $($x)c'est faux, n'est-ce pas? Oui: $($x)est faux car il s'exécute echo hello worldpuis essaie d'exécuter la sortie capturée (au moins dans les contextes où je pense que vous l'utilisez), qui échouera à moins que vous n'ayez un programme appelé hellokick around.
Jonathan Leffler
1
@tmow Ah, donc vous voulez réellement la fonctionnalité eval. Si c'est ce que vous voulez, vous pouvez utiliser eval; gardez simplement à l'esprit qu'il comporte de nombreuses mises en garde en matière de sécurité. C'est aussi le signe d'un défaut de conception dans votre application.
Zenexer
1
ref="${REF}_2" echo "${!ref}"exemple est faux, cela ne fonctionnera pas comme prévu car bash substitue des variables avant qu'une commande ne soit exécutée. Si la refvariable est vraiment indéfinie auparavant, le résultat de la substitution sera ref="VAR_2" echo "", et c'est ce qui sera exécuté.
Yoory N.19
17

Comment faire en evaltoute sécurité

eval peut être utilisé en toute sécurité - mais tous ses arguments doivent être cités en premier. Voici comment:

Cette fonction qui le fera pour vous:

function token_quote {
  local quoted=()
  for token; do
    quoted+=( "$(printf '%q' "$token")" )
  done
  printf '%s\n' "${quoted[*]}"
}

Exemple d'utilisation:

Compte tenu de certaines entrées utilisateur non fiables:

% input="Trying to hack you; date"

Construisez une commande pour évaluer:

% cmd=(echo "User gave:" "$input")

Évaluez-le, avec des citations apparemment correctes:

% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018

Notez que vous avez été piraté. datea été exécuté plutôt que d'être imprimé littéralement.

Au lieu de token_quote():

% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%

eval n'est pas mal - c'est juste mal compris :)

Tom Hale
la source
Comment la fonction "token_quote" utilise-t-elle ses arguments? Je ne trouve aucune documentation sur cette fonctionnalité ...
Akito
Je suppose que je l'ai formulé trop peu clairement. Je voulais dire les arguments de fonction. Pourquoi n'y a-t-il pas arg="$1"? Comment la boucle for sait-elle quels arguments ont été passés à la fonction?
Akito
J'irais plus loin que simplement "mal compris", c'est aussi souvent mal utilisé et vraiment inutile. La réponse de Zenexer couvre de nombreux cas de ce type, mais toute utilisation de evaldevrait être un drapeau rouge et doit être examinée de près pour confirmer qu'il n'y a vraiment pas de meilleure option déjà fournie par la langue.
dimo414 le