Por que a avaliação deve ser evitada no Bash e o que devo usar no lugar?

107

Vez após vez, vejo as respostas do Bash no Stack Overflow usando evale as respostas são destruídas, trocadilhos, pelo uso de uma construção tão "maligna". Por que é evaltão mau?

Se evalnão puder ser usado com segurança, o que devo usar no lugar?

Zenexer
fonte

Respostas:

148

Esse problema é mais do que aparenta. Começaremos com o óbvio: evaltem potencial para executar dados "sujos". Dados sujos são quaisquer dados que não foram reescritos como seguros para uso na situação-XYZ; no nosso caso, é qualquer string que não tenha sido formatada de forma a ser segura para avaliação.

A higienização de dados parece fácil à primeira vista. Supondo que estamos lançando uma lista de opções, o bash já fornece uma ótima maneira de higienizar elementos individuais e outra maneira de limpar todo o array como uma única string:

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

Agora, digamos que queremos adicionar uma opção para redirecionar a saída como um argumento para println. Podemos, é claro, apenas redirecionar a saída de println em cada chamada, mas, por exemplo, não faremos isso. Precisamos usar eval, já que as variáveis ​​não podem ser usadas para redirecionar a saída.

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.

Parece bom, certo? O problema é que eval analisa duas vezes a linha de comando (em qualquer shell). Na primeira passagem de análise, uma camada de citação é removida. Com as cotações removidas, algum conteúdo variável é executado.

Podemos corrigir isso permitindo que a expansão da variável ocorra dentro do eval. Tudo o que precisamos fazer é colocar tudo em aspas simples, deixando as aspas duplas onde estão. Uma exceção: temos que expandir o redirecionamento antes de eval, para que fique fora das aspas:

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.

Isso deve funcionar. Também é seguro, desde que $1em printlnnunca é sujo.

Agora espere um momento: eu uso a mesma sintaxe não citada que usamos originalmente com sudoo tempo todo! Por que funciona lá e não aqui? Por que tivemos que fazer aspas simples em tudo? sudoé um pouco mais moderno: sabe colocar entre aspas cada argumento que recebe, embora isso seja uma simplificação excessiva. evalsimplesmente concatena tudo.

Infelizmente, não há um substituto imediato para evalque trate argumentos como o sudofaz, pois evalé um shell embutido; isso é importante, pois assume o ambiente e o escopo do código circundante ao ser executado, em vez de criar uma nova pilha e escopo como uma função faz.

eval alternativas

Casos de uso específicos geralmente têm alternativas viáveis ​​para eval. Aqui está uma lista útil. commandrepresenta para onde você normalmente enviaria eval; substitua o que quiser.

No-op

Dois pontos simples são autônomos no bash:

:

Crie uma sub-casca

( command )   # Standard notation

Execute a saída de um comando

Nunca confie em um comando externo. Você deve estar sempre no controle do valor de retorno. Coloque em suas próprias linhas:

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

Redirecionamento baseado em variável

No código de chamada, mapeie &3(ou qualquer coisa maior que &2) para seu destino:

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!

Se fosse uma chamada única, você não teria que redirecionar todo o shell:

func arg1 arg2 3>&2

Dentro da função que está sendo chamada, redirecione para &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!

Indireção variável

Cenário:

VAR='1 2 3'
REF=VAR

Ruim:

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

Por quê? Se REF contiver aspas duplas, isso interromperá e abrirá o código para exploits. É possível higienizar REF, mas é uma perda de tempo quando você tem isso:

echo "${!REF}"

Isso mesmo, o bash tem indireção variável incorporada a partir da versão 2. É um pouco mais complicado do que evalse você quiser fazer algo mais complexo:

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

Independentemente disso, o novo método é mais intuitivo, embora possa não parecer assim para os programados experientes que estão acostumados eval.

Matrizes associativas

Arrays associativos são implementados intrinsecamente no bash 4. Uma advertência: eles devem ser criados usando 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

Em versões mais antigas do bash, você pode usar indireção variável:

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
fonte
4
Estou faltando uma menção de eval "export $var='$val'"... (?)
Zrin
1
@Zrin Provavelmente, não faz o que você espera. export "$var"="$val"é provavelmente o que você deseja. A única vez em que você pode usar seu formulário é se var='$var2'e quiser desreferenciá-lo duas vezes - mas não deve tentar fazer nada parecido no bash. Se você realmente precisa, você pode usar export "${!var}"="$val".
Zenexer
1
@anishsane: Suponha que, x="echo hello world";para executar o que quer que esteja contido x, podemos usar eval $xNo entanto, $($x)está errado, não é? Sim: $($x)está errado porque ele roda echo hello worlde tenta rodar a saída capturada (pelo menos nos contextos onde eu acho que você está usando), que irá falhar a menos que você tenha um programa chamado hellokicking around.
Jonathan Leffler
1
@tmow Ah, então você realmente quer a funcionalidade de avaliação. Se é isso que você deseja, pode usar eval; apenas lembre-se de que ele contém muitas advertências de segurança. Também é um sinal de que há uma falha de design em seu aplicativo.
Zenexer
1
ref="${REF}_2" echo "${!ref}"exemplo está errado, ele não funcionará como planejado, pois o bash substitui variáveis antes de um comando ser executado. Se a refvariável for realmente indefinida antes, o resultado da substituição será ref="VAR_2" echo "", e é isso que será executado.
Yoory N.
17

Como fazer evalseguro

eval pode ser usado com segurança - mas todos os seus argumentos precisam ser citados primeiro. Veja como:

Esta função que fará isso por você:

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

Exemplo de uso:

Dado alguma entrada de usuário não confiável:

% input="Trying to hack you; date"

Construa um comando para avaliar:

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

Avalie, com citações aparentemente corretas:

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

Observe que você foi hackeado. datefoi executado em vez de ser impresso literalmente.

Em vez de token_quote():

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

eval não é mau - é apenas mal compreendido :)

Tom Hale
fonte
Como a função "token_quote" usa seus argumentos? Não consigo encontrar nenhuma documentação sobre este recurso ...
Akito
Acho que formulei de maneira muito clara. Eu quis dizer os argumentos da função. Por que não existe arg="$1"? Como o loop for sabe quais argumentos foram passados ​​para a função?
Akito
Eu iria mais longe do que simplesmente "mal compreendido", também é frequentemente mal utilizado e realmente desnecessário. A resposta do Zenexer cobre muitos desses casos, mas qualquer uso de evaldeve ser um sinalizador vermelho e examinado de perto para confirmar que realmente não há uma opção melhor já fornecida pela linguagem.
dimo414