Mengapa eval harus dihindari di Bash, dan apa yang harus saya gunakan?

107

Berkali-kali, saya melihat jawaban Bash di Stack Overflow menggunakan evaldan jawaban dihancurkan, dimaksudkan, untuk penggunaan konstruksi "jahat" seperti itu. Mengapa evalbegitu jahat?

Jika evaltidak dapat digunakan dengan aman, apa yang harus saya gunakan?

Zenexer
sumber

Jawaban:

148

Ada lebih banyak masalah ini daripada yang terlihat. Kami akan mulai dengan yang sudah jelas: evalberpotensi untuk mengeksekusi data "kotor". Data kotor adalah data apa pun yang belum ditulis ulang sebagai data aman-untuk-digunakan-dalam-situasi-XYZ; dalam kasus kami, itu semua string yang belum diformat agar aman untuk evaluasi.

Sekilas data sanitasi tampak mudah. Dengan asumsi kita membuang-buang daftar opsi, bash sudah menyediakan cara yang bagus untuk membersihkan elemen individu, dan cara lain untuk membersihkan seluruh array sebagai satu 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).

Sekarang katakanlah kita ingin menambahkan opsi untuk mengarahkan keluaran sebagai argumen ke println. Kita bisa, tentu saja, hanya mengarahkan output println pada setiap panggilan, tapi sebagai contoh, kita tidak akan melakukannya. Kita harus menggunakan eval, karena variabel tidak dapat digunakan untuk mengarahkan keluaran.

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.

Terlihat bagus, bukan? Masalahnya adalah, eval mem-parsing dua kali baris perintah (di shell apa pun). Pada langkah pertama penguraian, satu lapisan kutipan dihapus. Dengan tanda kutip dihapus, beberapa konten variabel dieksekusi.

Kita dapat memperbaikinya dengan membiarkan ekspansi variabel berlangsung di dalam eval. Yang harus kita lakukan adalah kutipan tunggal semuanya, biarkan tanda kutip ganda di tempatnya. Satu pengecualian: kami harus memperluas pengalihan sebelum eval, sehingga harus tetap berada di luar tanda kutip:

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.

Ini seharusnya berhasil. Ini juga aman selama $1di printlntidak pernah kotor.

Sekarang tunggu sebentar: Saya menggunakan sintaks tanpa tanda kutip yang sama yang kita gunakan semula sudosepanjang waktu! Mengapa itu berhasil di sana, dan tidak di sini? Mengapa kita harus mengutip semuanya? sudosedikit lebih modern: ia tahu untuk memasukkan setiap argumen yang diterimanya dalam tanda kutip, meskipun itu adalah penyederhanaan yang berlebihan. evalhanya menggabungkan semuanya.

Sayangnya, tidak ada pengganti drop-in untuk evalyang memperlakukan argumen seperti sudohalnya, seperti evalshell built-in; ini penting, karena mengambil lingkungan dan cakupan kode sekitarnya saat dijalankan, daripada membuat tumpukan dan cakupan baru seperti fungsi.

eval Alternatif

Kasus penggunaan khusus sering kali memiliki alternatif yang layak eval. Berikut daftar praktisnya. commandmewakili apa yang biasanya Anda kirim eval; gantikan apapun yang Anda suka.

Tidak ada operasi

Titik dua sederhana tidak boleh digunakan dalam bash:

:

Buat sub-shell

( command )   # Standard notation

Jalankan keluaran dari sebuah perintah

Jangan pernah mengandalkan perintah eksternal. Anda harus selalu mengontrol nilai pengembalian. Letakkan ini di baris mereka sendiri:

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

Pengalihan berdasarkan variabel

Dalam kode panggilan, petakan &3(atau sesuatu yang lebih tinggi dari &2) ke target Anda:

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!

Jika itu adalah panggilan satu kali, Anda tidak perlu mengalihkan seluruh shell:

func arg1 arg2 3>&2

Di dalam fungsi yang dipanggil, alihkan ke &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!

Indireksi variabel

Skenario:

VAR='1 2 3'
REF=VAR

Buruk:

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

Mengapa? Jika REF berisi tanda kutip ganda, ini akan merusak dan membuka kode untuk dieksploitasi. Mungkin untuk membersihkan REF, tetapi membuang-buang waktu jika Anda memiliki ini:

echo "${!REF}"

Benar, bash memiliki variabel indirection built-in pada versi 2. Ini menjadi sedikit lebih rumit daripada evaljika Anda ingin melakukan sesuatu yang lebih kompleks:

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

Terlepas dari itu, metode baru ini lebih intuitif, meskipun mungkin tidak tampak seperti itu bagi terprogram berpengalaman yang terbiasa eval.

Array asosiatif

Array asosiatif diimplementasikan secara intrinsik di bash 4. Satu peringatan: mereka harus dibuat menggunakan 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

Di versi bash yang lebih lama, Anda dapat menggunakan variabel tidak langsung:

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
sumber
4
Saya melewatkan menyebutkan eval "export $var='$val'"... (?)
Zrin
1
@Zrin Kemungkinannya adalah itu tidak melakukan apa yang Anda harapkan. export "$var"="$val"mungkin yang Anda inginkan. Satu-satunya saat Anda mungkin menggunakan formulir Anda adalah jika var='$var2', dan Anda ingin membedakannya dua kali - tetapi Anda tidak boleh mencoba melakukan hal seperti itu dalam pesta. Jika memang harus, Anda bisa menggunakan export "${!var}"="$val".
Zenexer
1
@anishsane: Untuk Anda Misalkan, x="echo hello world";Lalu untuk mengeksekusi apa pun yang terkandung di dalamnya x, kita dapat menggunakan eval $xNamun, $($x)apakah salah, bukan? Ya: $($x)salah karena berjalan echo hello worlddan kemudian mencoba menjalankan output yang ditangkap (setidaknya dalam konteks di mana saya pikir Anda menggunakannya), yang akan gagal kecuali Anda memiliki program yang disebut hellokicking around.
Jonathan Leffler
1
@tmow Ah, jadi Anda sebenarnya ingin fungsionalitas eval. Jika itu yang Anda inginkan, Anda dapat menggunakan eval; hanya perlu diingat bahwa ada banyak peringatan keamanan. Ini juga merupakan tanda bahwa ada cacat desain dalam aplikasi Anda.
Zenexer
1
ref="${REF}_2" echo "${!ref}"Contoh salah, ini tidak akan berfungsi sebagaimana mestinya karena bash menggantikan variabel sebelum perintah dijalankan. Jika refvariabel benar-benar tidak terdefinisi sebelumnya, hasil substitusi akan menjadi ref="VAR_2" echo "", dan itulah yang akan dieksekusi.
Yoory N.
17

Bagaimana membuat evalaman

eval dapat digunakan dengan aman - tetapi semua argumennya harus dikutip terlebih dahulu. Begini caranya:

Fungsi ini yang akan melakukannya untuk Anda:

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

Contoh penggunaan:

Diberikan beberapa masukan pengguna yang tidak tepercaya:

% input="Trying to hack you; date"

Buat perintah untuk mengevaluasi:

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

Evaluasi itu, dengan kutipan yang tampaknya benar:

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

Perhatikan bahwa Anda diretas. datedieksekusi daripada dicetak secara harfiah.

Sebagai gantinya dengan token_quote():

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

eval tidak jahat - itu hanya disalahpahami :)

Tom Hale
sumber
Bagaimana fungsi "token_quote" menggunakan argumennya? Saya tidak dapat menemukan dokumentasi apa pun tentang fitur ini ...
Akito
Saya kira saya mengucapkannya terlalu tidak jelas. Maksud saya argumen fungsi. Mengapa tidak ada arg="$1"? Bagaimana perulangan for mengetahui argumen mana yang diteruskan ke fungsi?
Akito
Saya akan melangkah lebih jauh dari sekedar "salah paham", ini juga sering disalahgunakan dan benar-benar tidak diperlukan. Jawaban Zenexer mencakup banyak kasus seperti itu, tetapi penggunaan apa pun evalharus menjadi bendera merah dan diperiksa dengan cermat untuk mengonfirmasi bahwa sebenarnya tidak ada opsi yang lebih baik yang sudah disediakan oleh bahasa tersebut.
dimo414