Perché eval dovrebbe essere eval in Bash e cosa dovrei usare invece?


107

Di volta in volta, vedo le risposte di Bash su Stack Overflow che usano evale le risposte vengono colpite, gioco di parole, per l'uso di un costrutto "malvagio". Perché èeval così malvagio?

Se evalnon può essere usato in sicurezza, cosa dovrei usare invece?

Risposte:


148

C'è di più in questo problema di quanto non sembri. Inizieremo con l'ovvio: evalha il potenziale per eseguire dati "sporchi". I dati sporchi sono tutti i dati che non sono stati riscritti come XYZ sicuri per l'uso in situazioni; nel nostro caso, è qualsiasi stringa che non è stata formattata in modo da essere sicura per la valutazione.

La sanificazione dei dati sembra facile a prima vista. Supponendo di lanciare un elenco di opzioni, bash fornisce già un ottimo modo per disinfettare i singoli elementi e un altro modo per disinfettare l'intero array come una singola stringa:

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

Ora diciamo di voler aggiungere un'opzione per reindirizzare l'output come argomento a println. Potremmo, ovviamente, reindirizzare l'output di println su ogni chiamata, ma per amore di esempio, non lo faremo. Avremo bisogno di usare eval, poiché le variabili non possono essere utilizzate per reindirizzare l'output.

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.

Sembra buono, vero? Il problema è che eval analizza due volte la riga di comando (in qualsiasi shell). Al primo passaggio di analisi viene rimosso uno strato di citazioni. Con le virgolette rimosse, alcuni contenuti variabili vengono eseguiti.

Possiamo risolvere questo problema lasciando che l'espansione della variabile avvenga all'interno di eval. Tutto quello che dobbiamo fare è virgolette singole tutto, lasciando le virgolette doppie dove sono. Un'eccezione: dobbiamo espandere il reindirizzamento prima di eval, in modo che rimanga fuori dalle virgolette:

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.

Questo dovrebbe funzionare. È anche sicuro finché$1 in printlnnon è mai sporco.

Ora aspetta solo un momento: uso sempre la stessa sintassi non quotata che abbiamo usato originariamente sudo! Perché funziona lì e non qui? Perché abbiamo dovuto citare tutto in una sola volta? sudoè un po 'più moderno: sa racchiudere tra virgolette ogni argomento che riceve, anche se si tratta di una semplificazione eccessiva. evalconcatena semplicemente tutto.

Sfortunatamente, non esiste un sostituto immediato per evaltrattare gli argomenti come sudofa, comeeval è una shell incorporata; questo è importante, poiché assume l'ambiente e l'ambito del codice circostante quando viene eseguito, piuttosto che creare un nuovo stack e un nuovo ambito come fa una funzione.

eval alternative

Casi d'uso specifici spesso hanno valide alternative a eval. Ecco un pratico elenco. commandrappresenta ciò a cui spediresti normalmenteeval ; sostituisci quello che vuoi.

No-op

Un semplice due punti è un no-op in bash:

:

Crea una sottostruttura

( command )   # Standard notation

Esegue l'output di un comando

Non fare mai affidamento su un comando esterno. Dovresti sempre avere il controllo del valore restituito. Mettili sulle loro linee:

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

Reindirizzamento basato su variabile

Nel chiamare il codice, mappare &3(o qualcosa di più alto di &2) al tuo obiettivo:

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 una chiamata una tantum, non dovresti reindirizzare l'intera shell:

func arg1 arg2 3>&2

All'interno della funzione chiamata, reindirizza 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!

Indirizzamento indiretto variabile

Scenario:

VAR='1 2 3'
REF=VAR

Male:

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

Perché? Se REF contiene una virgoletta doppia, questo interromperà e aprirà il codice agli exploit. È possibile disinfettare REF, ma è una perdita di tempo quando hai questo:

echo "${!REF}"

Esatto, bash ha l'indirizzamento indiretto variabile integrato a partire dalla versione 2. Diventa un po 'più complicato che evalse vuoi fare qualcosa di più complesso:

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

Indipendentemente da ciò, il nuovo metodo è più intuitivo, anche se potrebbe non sembrare così ai programmati esperti che sono abituati eval.

Array associativi

Gli array associativi sono implementati intrinsecamente in bash 4. Un avvertimento: devono essere creati utilizzando 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

Nelle versioni precedenti di bash, puoi utilizzare l'indirizzamento indiretto delle variabili:

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"`"
# ...

4
Mi manca una menzione di eval "export $var='$val'"... (?)
Zrin

1
@Zrin È probabile che non faccia quello che ti aspetti. export "$var"="$val"è probabilmente quello che vuoi. L'unica volta in cui potresti usare il tuo modulo è se var='$var2', e vuoi dereferenziarlo due volte, ma non dovresti provare a fare nulla del genere in bash. Se proprio devi, puoi usare export "${!var}"="$val".
Zenexer

1
@anishsane: Per tua supposizione , x="echo hello world";quindi per eseguire tutto ciò che è contenuto x, possiamo usare eval $xTuttavia, $($x)è sbagliato, non è vero? Sì: $($x)è sbagliato perché viene eseguito echo hello worlde quindi tenta di eseguire l'output catturato (almeno nei contesti in cui penso che lo stai usando), che fallirà a meno che tu non abbia un programma chiamato hellokicking around.
Jonathan Leffler

1
@tmow Ah, quindi in realtà vuoi la funzionalità eval. Se è quello che vuoi, puoi usare eval; tieni presente che ha molti avvertimenti di sicurezza. È anche un segno che c'è un difetto di progettazione nella tua applicazione.
Zenexer

1
ref="${REF}_2" echo "${!ref}"esempio è sbagliato, non funzionerà come previsto poiché bash sostituisce le variabili prima che venga eseguito un comando. Se la refvariabile è davvero indefinita prima, il risultato della sostituzione sarà ref="VAR_2" echo "", ed è quello che verrà eseguito.
Yoory N.

17

Come mettere in evalsicurezza

eval può essere utilizzato in modo sicuro, ma tutti i suoi argomenti devono essere prima citati. Ecco come:

Questa funzione che lo farà per te:

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

Utilizzo di esempio:

Dato un input dell'utente non attendibile:

% input="Trying to hack you; date"

Costruisci un comando per valutare:

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

Valutalo, con citazioni apparentemente corrette:

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

Nota che sei stato violato. dateè stato eseguito anziché essere stampato letteralmente.

Invece con token_quote():

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

eval non è malvagio - è solo frainteso :)


In che modo la funzione "token_quote" utilizza i suoi argomenti? Non riesco a trovare alcuna documentazione su questa funzione ...
Akito


Immagino di averlo formulato in modo troppo poco chiaro. Intendevo gli argomenti della funzione. Perché non c'è arg="$1"? Come fa il ciclo for a sapere quali argomenti sono stati passati alla funzione?
Akito il

Vorrei andare oltre semplicemente "frainteso", è anche spesso usato in modo improprio e in realtà non è necessario. La risposta di Zenexer copre molti di questi casi, ma qualsiasi uso di evaldovrebbe essere una bandiera rossa ed esaminata attentamente per confermare che non esiste davvero un'opzione migliore già fornita dalla lingua.
dimo414
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.