¿Por qué debería evitarse eval en Bash y qué debería usar en su lugar?

107

Una y otra vez, veo las respuestas de Bash sobre el uso de Stack Overflow evaly las respuestas son criticadas, con un juego de palabras, por el uso de una construcción tan "malvada". ¿Por qué es evaltan malvado?

Si evalno se puede usar de manera segura, ¿qué debo usar en su lugar?

Zenexer
fuente

Respuestas:

148

Hay más en este problema de lo que parece. Comenzaremos con lo obvio: evaltiene el potencial de ejecutar datos "sucios". Los datos sucios son los datos que no se han reescrito como seguros para su uso en una situación XYZ; en nuestro caso, es cualquier cadena que no haya sido formateada para que sea segura para la evaluación.

La desinfección de datos parece fácil a primera vista. Suponiendo que estamos lanzando una lista de opciones, bash ya proporciona una excelente manera de desinfectar elementos individuales y otra forma de desinfectar toda la matriz como una sola cadena:

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).

Ahora digamos que queremos agregar una opción para redirigir la salida como argumento a println. Por supuesto, podríamos redirigir la salida de println en cada llamada, pero por ejemplo, no vamos a hacer eso. Necesitaremos usar eval, ya que las variables no se pueden usar para redirigir la salida.

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.

Se ve bien, ¿verdad? El problema es que eval analiza dos veces la línea de comando (en cualquier shell). En la primera pasada de análisis sintáctico, se elimina una capa de citas. Si se eliminan las comillas, se ejecuta parte del contenido variable.

Podemos solucionar esto dejando que la expansión de la variable tenga lugar dentro del eval. Todo lo que tenemos que hacer es comillas simples todo, dejando las comillas dobles donde están. Una excepción: tenemos que expandir la redirección antes de eval, por lo que debe quedar fuera de las comillas:

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.

Esto debería funcionar. También es seguro, siempre y cuando $1en printlnnunca es sucio.

Ahora, espere un momento: ¡uso la misma sintaxis sin comillas que usamos originalmente sudotodo el tiempo! ¿Por qué funciona allí y no aquí? ¿Por qué tuvimos que poner todo en comillas simples? sudoes un poco más moderno: sabe encerrar entre comillas cada argumento que recibe, aunque eso es una simplificación excesiva. evalsimplemente concatena todo.

Desafortunadamente, no hay un reemplazo directo para el evalque trata los argumentos como lo sudohace, ya que evales un shell incorporado; esto es importante, ya que toma el entorno y el alcance del código circundante cuando se ejecuta, en lugar de crear una nueva pila y alcance como lo hace una función.

eval Alternativas

Los casos de uso específicos a menudo tienen alternativas viables a eval. Aquí tienes una lista útil. commandrepresenta a lo que normalmente enviaría eval; sustitúyelo en lo que quieras.

Sin operación

Un simple colon no es una operación en bash:

:

Crear un subconjunto

( command )   # Standard notation

Ejecutar salida de un comando

Nunca confíe en un comando externo. Siempre debe tener el control del valor de retorno. Ponlos en sus propias líneas:

$(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.

Redirección basada en variable

En el código de llamada, asigne &3(o cualquier cosa superior a &2) a su objetivo:

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!

Si fuera una llamada única, no tendría que redirigir todo el shell:

func arg1 arg2 3>&2

Dentro de la función que se llama, redirigir a &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!

Indirección variable

Guión:

VAR='1 2 3'
REF=VAR

Malo:

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

¿Por qué? Si REF contiene una comilla doble, esto romperá y abrirá el código a exploits. Es posible desinfectar REF, pero es una pérdida de tiempo cuando tiene esto:

echo "${!REF}"

Así es, bash tiene incorporada la indirección variable a partir de la versión 2. Se vuelve un poco más complicado que evalsi quisieras hacer algo más complejo:

# 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\""

Independientemente, el nuevo método es más intuitivo, aunque puede que no lo parezca a los programados experimentados que están acostumbrados eval.

Matrices asociativas

Las matrices asociativas se implementan intrínsecamente en bash 4. Una advertencia: deben crearse utilizando 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

En versiones anteriores de bash, puede usar direccionamiento indirecto 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
fuente
4
Me falta una mención de eval "export $var='$val'"... (?)
Zrin
1
@Zrin Lo más probable es que no haga lo que esperabas. export "$var"="$val"es probablemente lo que quieres. La única vez que puede usar su formulario es si var='$var2', y desea desreferenciarlo dos veces, pero no debería intentar hacer algo así en bash. Si realmente debe hacerlo, puede usar export "${!var}"="$val".
Zenexer
1
@anishsane: Supongamos que, x="echo hello world";para ejecutar lo que sea que contenga x, podemos usar eval $xSin embargo, $($x)está mal, ¿no? Sí: $($x)está mal porque se ejecuta echo hello worldy luego intenta ejecutar la salida capturada (al menos en los contextos donde creo que la estás usando), que fallará a menos que tengas un programa llamado hellopateando.
Jonathan Leffler
1
@tmow Ah, entonces realmente quieres la funcionalidad de evaluación. Si eso es lo que quiere, puede usar eval; solo tenga en cuenta que tiene muchas advertencias de seguridad. También es una señal de que hay una falla de diseño en su aplicación.
Zenexer
1
ref="${REF}_2" echo "${!ref}"El ejemplo es incorrecto, no funcionará como se esperaba ya que bash sustituye las variables antes de que se ejecute un comando. Si la refvariable es realmente indefinida antes, el resultado de la sustitución será ref="VAR_2" echo "", y eso es lo que se ejecutará.
Yoory N.
17

Cómo hacer evalseguro

eval puede usarse con seguridad, pero todos sus argumentos deben citarse primero. Así es cómo:

Esta función que lo hará por ti:

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

Uso de ejemplo:

Dada alguna entrada de usuario no confiable:

% input="Trying to hack you; date"

Construya un comando para evaluar:

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

Evalúelo, con citas aparentemente correctas:

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

Tenga en cuenta que fue pirateado. datese ejecutó en lugar de imprimirse literalmente.

En su lugar con token_quote():

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

eval no es malvado, simplemente se malinterpreta :)

Tom Hale
fuente
¿Cómo usa la función "token_quote" sus argumentos? No puedo encontrar ninguna documentación sobre esta función ...
Akito
Supongo que lo expresé de forma muy poco clara. Me refiero a los argumentos de la función. ¿Por qué no arg="$1"? ¿Cómo sabe el ciclo for qué argumentos se pasaron a la función?
Akito
Yo iría más allá de simplemente "incomprendido", también a menudo se usa mal y realmente no es necesario. La respuesta de Zenexer cubre muchos de estos casos, pero cualquier uso de evaldebería ser una señal de alerta y examinarse de cerca para confirmar que realmente no existe una mejor opción que el lenguaje ya haya proporcionado.
dimo414