فرمان eval برای سوءاستفاده به شدت قدرتمند و بینهایت آسان است.
باعث میشود کُد شما به جای یکبار دوبار تجزیه بشود، این به معنی آنست که برای مثال، اگر کُد شما دارای متغیر مرجع باشد، تفکیک کننده پوسته، محتوای آن متغیر را ارزیابی خواهد نمود. اگر متغیر محتوی فرمان پوسته باشد، پوسته میتواند آن فرمان را اجرا کند، آیا شما میخواهید اینطور باشد یا خیر. این مطلب میتواند به نتایج غیر منتظره منجر گردد، مخصوصاً موقعی که متغیرها بتوانند از منابع غیرقابل اعتماد خوانده شوند(مانند کاربران یا فایلهای تولید شده کاربران).
"eval" غلط املایی رایج evil(مترجم: زیانبار، ناشناخته) است. بخشی از این پرسش و پاسخ که به فاصلهها در نام فایلها میپردازد، قبلاً شامل اسکریپت زیر تحت عنوان «ابزار مفید(که احتمالاً به اندازه تکنیک
Syntax : nasty_find_all <path> <command> [maxdepth]
# !این کد زیانبار است و هرگز نباید به کار برود export IFS=" " [ -z "$3" ] && set -- "$1" "$2" 1 FILES=`find "$1" -maxdepth "$3" -type f -printf "\"%p\" "` # هشدار، شرارت eval FILES=($FILES) for ((I=0; I < ${#FILES[@]}; I++)) do eval "$2 \"${FILES[I]}\"" done unset IFS
این اسکریپت جستجوی بازگشتی فایلها و اجرای فرمان معین شده کاربر روی آنها، حتی اگر نام آنها شامل سطر جدید یا فاصله یا هردو باشد را فرض کرده بود. مؤلف گمان کرده بود که find -print0 | xargs -0 برای مقاصدی از قبیل فرمانهای چندگانه مناسب نمیباشد. این اسکریپت با یک توضیح تعلیمی درهریک از تمام سطرهای در بر گیرنده همراه بود، که ما از آنها صرفنظر میکنیم.
در دفاع از اسکریپت، به این شکل کار میکند:
$ ls -lR .: total 8 drwxr-xr-x 2 vidar users 4096 Nov 12 21:51 dir with spaces -rwxr-xr-x 1 vidar users 248 Nov 12 21:50 nasty_find_all ./dir with spaces: total 0 -rw-r--r-- 1 vidar users 0 Nov 12 21:51 file?with newlines $ ./nasty_find_all . echo 3 ./nasty_find_all ./dir with spaces/file with newlines $
اما این را ملاحظه نمایید:
$ touch "\"); ls -l $'\x2F'; #"
شما درست فایلی به نام "); ls -l $'\x2F'; # ایجاد نمودهاید.
حالا
FILES=(""); ls -l $'\x2F'; #"
که خودش میشود دوجمله FILES=(""); و ls -l / تبریک، شما دقیقاً اجرای دستورات دلخواه را مجاز نمودهاید.
$ touch "\"); ls -l $'\x2F'; #" $ ./nasty_find_all . echo 3 total 1052 -rw-r--r-- 1 root root 1018530 Apr 6 2005 System.map drwxr-xr-x 2 root root 4096 Oct 26 22:05 bin drwxr-xr-x 3 root root 4096 Oct 26 22:05 boot drwxr-xr-x 17 root root 29500 Nov 12 20:52 dev drwxr-xr-x 68 root root 4096 Nov 12 20:54 etc drwxr-xr-x 9 root root 4096 Oct 5 11:37 home drwxr-xr-x 10 root root 4096 Oct 26 22:05 lib drwxr-xr-x 2 root root 4096 Nov 4 00:14 lost+found drwxr-xr-x 6 root root 4096 Nov 4 18:22 mnt drwxr-xr-x 11 root root 4096 Oct 26 22:05 opt dr-xr-xr-x 82 root root 0 Nov 4 00:41 proc drwx------ 26 root root 4096 Oct 26 22:05 root drwxr-xr-x 2 root root 4096 Nov 4 00:34 sbin drwxr-xr-x 9 root root 0 Nov 4 00:41 sys drwxrwxrwt 8 root root 4096 Nov 12 21:55 tmp drwxr-xr-x 15 root root 4096 Oct 26 22:05 usr drwxr-xr-x 13 root root 4096 Oct 26 22:05 var ./nasty_find_all ./dir with spaces/file with newlines ./ $
تعویض ls -l با rm -rf یا وخیمتر، قدرت خلاقه زیادی نمیخواهد.
کسی ممکن است با خود بگوید اینها رویدادهای مشکوک است، اما کسی این نیرنگ را به کار نمیبرد. تمام آن فقط یک کاربر بداندیش میخواهد، یا شاید محتملتر، کاربر مبتدی که موقع رفتن به حمام ترمینال خود را قفل نشده ترک میکند، یا اسکریپت PHP نوشته شده برای ارسال فایل که سلامت نام فایل را کنترل نمیکند، یا فردی مرتکب اشتباهی مانند شخصی بشود که خودش اجازه اجرای کُد اختیاری را جایز نموده است(اکنون به جای محدود بودن به کاربران وب ، یک ضارب میتواند از nasty_find_all برای عبور به محبس chroot و به دست آوردن امتیازات اضافی استفاده کند)، یا استفاده از یک سرویس گیرنده IRC یا IM که در پذیرش نام فایلها برای انتقال فایل یا لاگهای مکالمه بیش از حد آزادیخواه است، و غیره.
رایجترین استفاده صحیح از فرمان eval خواندن متغیرها از خروجی برنامهایست که مخصوصاً طراحی شده برای استفاده به این طریق است. برای مثال،
# در سیتمهای قدیمی، بعد از تغییر اندازه پنجره، شخص این را باید اجرا کند eval `resize` # SSH کمتر ابتدایی: گرفتن عبارت عبور برای یک کلید محرمانه # اجرا میشود .xsession یا .profile این به طور نوعی از یک نوع # به تمام پردازشهای نشست کاربر ssh متغیرهای تولید شده توسط کارگزار # .احتمالی از آن ارث میبرد ssh صادر میشوند، به طوری که یک eval `ssh-agent -s`
eval کاربردهای دیگری دارد، مخصوصاً موقع ایجاد متغیرهای فوقالعاده(متغیرهای غیرمستقیم مرجع). این هم یک مثال از یک روش تجزیه گزینههای خط فرمان که پارامترها را نمیپذیرد:
# POSIX # # تولید متغیرهای گزینه به طور پویا، احظار ش را امتحان کنید # # sh -x example.sh --verbose --test --debug for i in "$@" do case "$i" in --test|--verbose|--debug) shift # حذف گزینه خط فرمان name=${i#--} # حذف پیشوند گزینه eval "$name='$name'" # از نو ساختن متغیر ;; esac done echo "verbose: $verbose" echo "test: $test" echo "debug: $debug"
بنابراین، چرا این نگارش قابل پذیرش است؟ قابل پذیرش است به دلیل آنکه ما فرمان eval را به طوری محدود کردهایم که فقط موقعی اجرا خواهد شد که ورودی یکی از مقادیر معلوم مجموعه محدود باشد. بنابراین، هرگز نمیتواند توسط کاربر برای اجرای دستور اختیاری مورد سوءاستفاده قرار گیرد -- هر ورودی توأم با دغلبازی، با یکی از سه ورودی مجاز از پیش تعریف شده مطابقت نخواهد کرد. این مغایرت قابل قبول نخواهد بود:
# !کُد خطرناک، این را به کار نبرید for i in "$@" do case "$i" in --test*|--verbose*|--debug*) shift # حذف گزینه خط فرمان name=${i#--} # حذف پیشوند گزینه eval "$name='$name'" # درست کردن گزینه از نو ;; esac done
تمام آنچه تغییر نموده آنست که ما سعی نمودهایم مثال خوب قبلی(که کار خیلی زیادی انجام نمیدهد) را به این طریق ، با اجازه دادن به دریافت مواردی مانند --test=foo سودمند کنیم. اما نگاه کنید که چه چیزی را فعال میسازد:
$ ./foo --test='; ls -l /etc/passwd;x=' -rw-r--r-- 1 root root 943 2007-03-28 12:03 /etc/passwd
یکبار دیگر: با مجاز نمودن آنکه فرمان eval با ورودی فیلتر نشده کاربر استفاده بشود، ما اجرای فرمان دلخواه را مجاز کردهایم.
اگر چه، تخصیص یک متغیر اختیاری توسط eval با استفاده از این ترکیب دستوری کاملاً بی خطر میباشد:
eval "$varname=\$whatever"
البته، این ترکیب فرض میکند که $varname نام یک متغیر معتبر میباشد.
آیا این نمیتوانست با declare بهتر انجام بشود؟ به عنوان مثال:
for i in "$@" do case "$i" in --test|--verbose|--debug) shift # حذف گزینه خط فرمان name=${i#--} # حذف پیشوند گزینه declare $name=Yes # تنظیم مقدار پیش فرض ;; --test=*|--verbose=*|--debug=*) shift name=${i#--} value=${name#*=} # کمیت جایی بعد از اولین کلمه و = است name=${name%%=*} # محدود نمودن نام به تنها یک کلمه # (حتی اگر یک = دیگر در مقدار باشد) declare $name="$value" # درست کردن گزینه از نو ;; esac done
توجه کنید که --name برای حالت پیش فرض ، و --name=value قالبهای لازم هستند.
به نظر میرسد declare نوعی تفکیک کننده جادویی دارد، بیشتر مانند [[ . در اینجا آزمایشی هست که من در bash نگارش 3.1.17 انجام دادهام:
griffon:~$ declare foo=x;date;x=Yes Sun Nov 4 09:36:08 EST 2007 griffon:~$ name='foo=x;date;x' griffon:~$ declare $name=Yes griffon:~$ echo $foo x;date;x=Yes
آشکار است که، حداقل در bash, فرمان declare خیلی مطمئنتر از eval است.
attoparsec:~$ echo $BASH_VERSION 4.2.24(1)-release attoparsec:~$ danger='( $(printf "%s!\n" DANGER >&2) )' attoparsec:~$ declare safe=${danger} attoparsec:~$ declare -a unsafe attoparsec:~$ declare unsafe=${danger} DANGER! attoparsec:~$متغیرهای عادی ممکن است با declare بی خطر باشند، اما متغیرهای آرایهای بیخطر نیستند.
برای لیستی از روشهای ارجاع یا مقداردهی غیر مستقیم متغیرها بدون استفاده از eval، لطفاً پرسش و پاسخ شماره 6 را ببینید. (این بخش قبل از آن پاسخ نوشته شده بود، اما من آن را به عنوان یک منبع در اینجا قرار دادهام.)
یک رویکرد دیگر آن است که کُد خطرناک میتواند در یک تابع پوشانیده بشود. برای مثال به جای انجام کاری مانند این:
eval "${ArrayName}"'="${Value}"'
حال آنکه مثال فوق به طور قابل قبولی صحیح است، اما هنوز قابلیت آسیبپذیری دارد. توجه کنید چه اتفاقی میافتد اگر به صورت زیر انجام بدهیم.
ArrayName="echo rm -rf /tmp/dummyfolder/*; tvar" eval "${ArrayName}"'="${Value}"'
راه پیشگیری از این گونه حفره امنیتی ایجاد تابعی است که اجرایش مقدار معینی از امنیت را به شما میدهد و کُد پاکیزهتری را جایز مینماید.
# check_valid_var_name VariableName function check_valid_var_name { case "${1:?Missing Variable Name}" in [!a-zA-Z_]* | *[!a-zA-Z_0-9]* ) return 3;; esac } # set_variable VariableName [<Variable Value>] function set_variable { check_valid_var_name "${1:?Missing Variable Name}" || return $? eval "${1}"'="${2:-}"' } set_variable "laksdpaso" "dasädöas# #-c,c pos 9302 1´ " set_variable "echo rm -rf /tmp/dummyfolder/*; tvar" "dasädöas# #-c,c pos 9302 1´ " # return Error
توجه: set_variable یک مزیت اضافه بر کاربرد declare دارد. مورد پایین را ملاحظه کنید.
VariableName="Name=hhh" declare "${VariableName}=Test Value" # Valid code, unexpected behavior set_variable "${VariableName}" "Test Value" # return Error
چند مثال دیگر برای ارجاع
# get_array_element VariableName ArrayName ArrayElement function get_array_element { check_valid_var_name "${1:?Missing Variable Name}" || return $? check_valid_var_name "${2:?Missing Array Name}" || return $? eval "${1}"'="${'"${2}"'["${3:?Missing Array Index}"]}"' } # set_array_element ArrayName ArrayElement [<Variable Value>] function set_array_element { check_valid_var_name "${1:?Missing Array Name}" || return $? eval "${1}"'["${2:?Missing Array Index}"]="${3:-}"' } # unset_array_element ArrayName ArrayElement function unset_array_element { unset "${1}[${2}]" } # unset_array_element VarName ArrayName function get_array_element_cnt { check_valid_var_name "${1:?Missing Variable Name}" || return $? check_valid_var_name "${2:?Missing Array Name}" || return $? eval "${1}"'="${#'"${2}"'[@]}"' } # push_element ArrayName <New Element 1> [<New Element 2> ...] function push_element { check_valid_var_name "${1:?Missing Array Name}" || return $? local ArrayName="${1}" local LastElement eval 'LastElement="${#'"${ArrayName}"'[@]}"' while shift && [ $# -gt 0 ] ; do eval "${ArrayName}"'["${LastElement}"]="${1}"' let LastElement+=1 done } # pop_element ArrayName <Destination Variable Name 1> [<Destination Variable Name 2> ...] function pop_element { check_valid_var_name "${1:?Missing Array Name}" || return $? local ArrayName="${1}" local LastElement eval 'LastElement="${#'"${ArrayName}"'[@]}"' while shift && [[ $# -gt 0 && ${LastElement} -gt 0 ]] ; do let LastElement-=1 check_valid_var_name "${1:?Missing Variable Name}" || return $? eval "${1}"'="${'"${ArrayName}"'["${LastElement}"]}"' unset "${ArrayName}[${LastElement}]" done [[ $# -eq 0 ]] || return 8 } # shift_element ArrayName [<Destination Variable Name>] function shift_element { check_valid_var_name "${1:?Missing Array Name}" || return $? local ArrayName="${1}" local CurElement=0 LastElement eval 'LastElement="${#'"${ArrayName}"'[@]}"' while shift && [[ $# -gt 0 && ${LastElement} -gt ${CurElement} ]] ; do check_valid_var_name "${1:?Missing Variable Name}" || return $? eval "${1}"'="${'"${ArrayName}"'["${CurElement}"]}"' let CurElement+=1 done eval "${ArrayName}"'=("${'"${ArrayName}"'[@]:${CurElement}}")' [[ $# -eq 0 ]] || return 8 } # unshift_element ArrayName <New Element 1> [<New Element 2> ...] function unshift_element { check_valid_var_name "${1:?Missing Array Name}" || return $? [ $# -gt 1 ] || return 0 eval "${1}"'=("${@:2}" "${'"${1}"'[@]}" )' } # 1000 x { declare "laksdpaso=dasädöas# #-c,c pos 9302 1´ " } 0m0.069s مدت زمان اجرا # 1000 x { set_variable laksdpaso "dasädöas# #-c,c pos 9302 1´ " } 0m0.141s مدت زمان اجرا # 1000 x { get_array_element TestVar TestArray 1 } 0m0.199s مدت زمان اجرا # 1000 x { set_array_element TestArray 1 "dfds edfs fdf df" } 0m0.174s مدت زمان اجرا # 1000 x { set_array_element TestArray 0 } 0m0.167s مدت زمان اجرا # 1000 x { get_array_element_cnt TestVar TestArray } 0m0.171s مدت زمان اجرا # با یک آرایه دو هزار عنصری اتجام شدهاند push,pops,shifts,unshifts همه نوابع # 1000 x { push_element TestArray "dsf sdf ss s" } 0m0.274s مدت زمان اجرا # 1000 x { pop_element TestArray TestVar } 0m0.380s مدت زمان اجرا # 1000 x { unshift_element TestArray "dsf sdf ss s" } 0m9.027s مدت زمان اجرا # 1000 x { shift_element TestArray TestVar } 0m5.583s مدت زمان اجرا
توجه، shift_element و unshift_element کارایی ضعیفی دارند و مخصوصاً روی آرایههای بزرگ باید از آنها اجتناب گردد. مابقی کارایی قابل قبولی دارند و من به طور منظم آنها را به کار میبرم.
پرسش و پاسخ 48 (آخرین ویرایش 2013-03-07 03:51:23 توسط ChrisJohnson)