Чому слід уникати eval в Bash, і що я повинен використовувати замість цього?


107

Знову і знову, я бачу відповіді Баша про переповнення стека, evalі відповіді отримують грубі, призначені каламбури, для використання такої "злої" конструкції. Чому evalтак зло?

Якщо evalне можна використовувати безпечно, що я повинен використовувати замість цього?

Відповіді:


148

Цієї проблеми є більше, ніж очі. Почнемо з очевидного: чи evalє потенціал для виконання "брудних" даних. Брудні дані - це будь-які дані, які не переписані як безпечні для використання в ситуації-XYZ; у нашому випадку це будь-яка рядок, яка не була відформатована так, щоб бути безпечною для оцінки.

Санітизація даних здається простою на перший погляд. Якщо припустити, що ми обробляємо список варіантів, bash вже пропонує чудовий спосіб оздоровити окремі елементи, а також інший спосіб оздоровити весь масив як єдину рядок:

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

Тепер скажімо, що ми хочемо додати параметр для переадресації виводу як аргумент для println. Ми, звичайно, могли просто перенаправити вихід println на кожен виклик, але заради прикладу ми цього не збираємось робити. Нам потрібно буде використовувати eval, оскільки змінні не можна використовувати для перенаправлення виводу.

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.

Виглядає добре, правда? Проблема полягає в тому, що eval аналізує вдвічі командний рядок (у будь-якій оболонці). При першому проході розбору видаляється один шар цитування. Коли цитати вилучені, деякі змінні вмісту виконуються.

Ми можемо це виправити, дозволяючи змінній розширення відбуватися в межах eval. Все, що нам потрібно зробити, - це процитувати все, залишивши подвійні лапки там, де вони є. Один виняток: ми маємо розширити перенаправлення до eval, так що це повинно залишатися поза цитатами:

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.

Це має спрацювати. Це також безпечно до тих пір, поки $1в printlnніколи не брудниться.

Тепер затримайтеся лише на мить: я використовую той самий котируваний синтаксис, який ми використовували спочатку sudoвесь час! Чому це працює там, а не тут? Чому нам довелося все процитувати? sudoє трохи сучаснішим: він знає вкладати в лапки кожен отриманий аргумент, хоча це є надмірним спрощенням. evalпросто об'єднує все.

На жаль, не існує заміни для evalвипаду, яка б розглядає аргументи як sudodo, як evalі вбудована оболонка; це важливо, оскільки воно виконує середовище та обсяг навколишнього коду під час його виконання, а не створює новий стек та область, як це робить функція.

eval Альтернативи

У конкретних випадках використання часто є життєздатні альтернативи eval. Ось зручний список. commandпредставляє те, до чого ви зазвичай надсилаєте eval; замінюйте чим завгодно.

Не-оп

Проста двокрапка є неоперативною в баші:

:

Створіть під-оболонку

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

Перенаправлення на основі змінної

Викликаючи код, нанесіть на карту &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 "$1" 00000000`"
    echo -n "${key##*$}"
}

local var_key="VAR_`mkkey "$key"`"
# ...

4
Я пропускаю згадку про eval "export $var='$val'"... (?)
Зрін

1
@Zrin Швидше за все, це не те, що ви очікуєте. export "$var"="$val"це, мабуть, те, що ти хочеш. Єдиний раз, коли ви можете скористатись формою, це якщо var='$var2', і ви хочете подвоїти її відновлення - але вам не слід намагатися робити щось подібне в баш. Якщо ви дійсно повинні, можете використовувати export "${!var}"="$val".
Zenexer

1
@anishsane: Для вашого припущення , x="echo hello world";тоді для виконання всього, що міститься x, ми можемо використовувати. eval $xОднак $($x)це неправильно, чи не так? Так: $($x)неправильно, оскільки він працює, echo hello worldа потім намагається запустити захоплений вихід (принаймні, в контекстах, де я думаю, що ви його використовуєте), що не вдасться, якщо у вас немає програми під назвою helloбрикання.
Джонатан Леффлер

1
@tmow Ах, значить, ти дійсно хочеш функціоналу eval. Якщо це те, що ви хочете, тоді ви можете використовувати eval; просто пам’ятайте, що в ньому є багато застережень щодо безпеки. Це також знак того, що у вашій програмі є недолік дизайну.
Zenexer

1
ref="${REF}_2" echo "${!ref}"Приклад помилковий, він не працюватиме за призначенням, оскільки bash замінює змінні перед виконанням команди. Якщо refзмінна дійсно раніше не визначена, результат підстановки буде ref="VAR_2" echo "", і саме це буде виконано.
Yoory N.

17

Як зробити 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 "$(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 не зло - це просто неправильно :)


Як функція "token_quote" використовує свої аргументи? Я не можу знайти жодної документації щодо цієї функції ...
Akito


Думаю, я сформулював це занадто неясно. Я мав на увазі аргументи функції. Чому немає arg="$1"? Як цикл for знає, які аргументи передані функції?
Акіто

Я б пішов далі, ніж просто "неправильно зрозуміли", це також часто нецільово використовуються і справді не потрібні. Відповідь Зенексера охоплює дуже багато таких випадків, але будь-яке використанняeval має бути червоним прапором і ретельно вивчене, щоб підтвердити, чи дійсно не є кращим варіантом, який вже надається мовою.
dimo414
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.