it-swarm.dev

لماذا يجب تجنب eval في Bash ، وما الذي يجب استخدامه بدلاً من ذلك؟

مرارًا وتكرارًا ، أرى إجابات Bash على Stack Overflow باستخدام eval وتتعرض الإجابات للكسر ، وتهدف التورية ، لاستخدام مثل هذا البناء "الشرير". لماذا eval شرير جدًا؟

إذا لم يكن بالإمكان استخدام eval بأمان ، فما الذي يجب استخدامه بدلاً من ذلك؟

80
Zenexer

هناك المزيد لهذه المشكلة مما تراه العين. سنبدأ بالواضح: 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 على كل مكالمة ، لكن من أجل المثال ، لن نقوم بذلك. سنحتاج إلى استخدام 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 بتوزيع مرتين سطر الأوامر (في أي Shell). في المقطع الأول للتحليل تتم إزالة طبقة واحدة من الاقتباس. مع إزالة علامات الاقتباس ، يتم تنفيذ بعض المحتويات المتغيرة.

يمكننا إصلاح ذلك عن طريق السماح للتوسع المتغير أن يحدث داخل 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 يعامل وسيطات مثل Sudo ، كما هو الحال في eval فهو مدمج في Shell ؛ هذا أمر مهم ، لأنه يأخذ البيئة ونطاق الكود المحيط عندما ينفذ ، بدلاً من إنشاء مكدس ونطاق جديد كما تفعل الوظيفة.

بدائل 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!

إذا كانت مكالمة لمرة واحدة ، فلن تضطر إلى إعادة توجيه Shell بالكامل:

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

كيفية جعل 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 ليس شرًا - إنه يساء فهمه فقط :)

10
Tom Hale