Почему eval следует избегать в Bash, и что я должен использовать вместо этого?
снова и снова я вижу ответы Bash на переполнение стека с помощью eval
и ответы получают удар, каламбур, предназначенный для использования такой "злой" конструкции. Почему это eval
так зло?
Если eval
не может использоваться безопасно, что я должен использовать вместо этого?
3 ответов
в этой проблеме больше, чем кажется на первый взгляд. Начнем с очевидного:--14--> имеет потенциал для выполнения "грязных" данных. Грязные данные-это любые данные, которые не были переписаны как безопасные для использования в ситуации XYZ; в нашем случае это любая строка, которая не была отформатирована так, чтобы быть безопасной для оценки.
дезинфицировать данные кажется легким на первый взгляд. Предполагая, что мы разбрасываем список вариантов, bash уже предоставляет отличный способ дезинфицировать человека элементы и другой способ дезинфицировать весь массив как одну строку:
function println
{
# Send each element as a separate argument, starting with the second element.
# Arguments to printf:
# 1 -> "\n"
# 2 -> ""
# 3 -> ""
# 4 -> ""
# etc.
printf "\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 -> ""
# 3 -> "${*:2}"
println '\e[31mError (%d): %s\e[m' "" "${*:2}"
exit ""
}
# 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).
теперь скажем, что мы хотим добавить опцию перенаправления вывода в качестве аргумента для println. Мы могли бы, конечно, просто перенаправить вывод println на каждый вызов, но, например, мы не собираемся этого делать. Нам нужно использовать eval
, поскольку переменные не могут использоваться для перенаправления вывода.
function println
{
eval printf "\n" "${@:3}"
}
function error
{
println '>&2' '\e[31mError (%d): %s\e[m' "" "${*:2}"
exit
}
error 1234 Something went wrong.
выглядит хорошо, верно? Проблема в том, что eval анализирует дважды командную строку (в любом оболочка.) При первом проходе синтаксического анализа удаляется один слой цитирования. При удалении кавычек выполняется некоторое содержимое переменной.
мы можем исправить это, позволив переменному расширению иметь место в eval
. Все, что нам нужно сделать, это одинарные кавычки, оставляя двойные кавычки там, где они есть. Одно исключение: мы должны расширить перенаправление до eval
, так что должен оставаться вне кавычек:
function println
{
eval 'printf "\n" "${@:3}"'
}
function error
{
println '&2' '\e[31mError (%d): %s\e[m' "" "${*:2}"
exit
}
error 1234 Something went wrong.
это должно работать. Это также безопасно, как пока на
println
никогда не бывает грязной.
теперь подождите минутку: я использую то же самое без кавычек синтаксис, который мы использовали изначально с sudo
все время! Почему он работает там, а не здесь? Почему мы должны были все цитировать? sudo
немного более современно: он знает, чтобы заключить в кавычки каждый аргумент, который он получает, хотя это чрезмерное упрощение. eval
просто объединяет все.
к сожалению, нет никакой замены дляeval
это относится к аргументам как sudo
, как eval
- это встроенная оболочка; это важно, поскольку она принимает среду и область окружающего кода при его выполнении, а не создает новый стек и область, как это делает функция.
eval Alternatives
конкретные случаи использования часто имеют реальных альтернатив eval
. Вот удобный список. command
представляет то, что вы обычно посылаете в eval
; замените в чем угодно.
No-op
простой двоеточие в no-op в bash: :
создать под-оболочку
( command ) # Standard notation
выполнить вывод команды
никогда не полагайтесь на внешнюю команду. Вы всегда должны контролировать возвращаемое значение. Положите это на свои собственные строки:
$(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.
перенаправление на основе переменной
в вызывающем коде, map &3
(или что-нибудь выше, чем &2
) к вашей цели:
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!
если бы это был одноразовый вызов, вам не пришлось бы перенаправлять всю оболочку:
func arg1 arg2 3>&2
в вызываемой функции перенаправить на &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!
переменной косвенности
сценарий:
VAR='1 2 3'
REF=VAR
плохое:
eval "echo \"$$REF\""
почему? Если REF содержит двойную кавычку, это приведет к разрыву и открытию кода для эксплойтов. Можно дезинфицировать REF, но это пустая трата времени, когда у вас есть это:
echo "${!REF}"
правильно, bash имеет переменную косвенность, встроенную с версии 2. Это становится немного сложнее, чем eval
если вы хотите сделать что-то более сложное:
# 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\""
независимо от того, новый метод более интуитивно понятен, хотя может показаться, что это не так для опытных программистов, которые привыкли eval
.
ассоциативные массивы
ассоциативные массивы реализованы внутренне в bash 4. Одно предостережение: они должны создаваться с помощью 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
в более старых версиях bash вы можете использовать переменную косвенность:
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 "" 00000000`"
echo -n "${key##*$}"
}
local var_key="VAR_`mkkey "$key"`"
# ...
Как сделать eval
безопасное
eval
можете безопасно использовать - но все его аргументы должны быть указаны в первую очередь. Вот как:
эта функция, которая сделает это за вас:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
пример использования:
учитывая некоторые ненадежные пользовательские данные:
% input="Trying to hack you; date"
создайте команду для eval:
% cmd=(echo "User gave:" "$input")
Eval его, с казалось бы исправить цитата:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
обратите внимание, что вы были взломаны. date
был выполнен, а не напечатан буквально.
вместо с token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
это не зло - это просто неправильно понято:)