Tại sao nên tránh eval trong Bash và tôi nên sử dụng cái gì thay thế?


107

Hết lần này đến lần khác, tôi thấy các câu trả lời Bash trên Stack Overflow bằng cách sử dụng evalvà các câu trả lời bị đánh lừa, nhằm mục đích chơi chữ, để sử dụng một cấu trúc "xấu xa" như vậy. Sao evalác thế?

Nếu evalkhông thể sử dụng một cách an toàn, tôi nên dùng gì để thay thế?

Câu trả lời:


148

Vấn đề này còn nhiều thứ hơn là gặp mắt. Chúng ta sẽ bắt đầu với điều hiển nhiên: evalcó khả năng thực thi dữ liệu "bẩn". Dữ liệu bẩn là bất kỳ dữ liệu nào chưa được viết lại dưới dạng an toàn cho việc sử dụng trong tình huống-XYZ; trong trường hợp của chúng tôi, đó là bất kỳ chuỗi nào chưa được định dạng để an toàn cho việc đánh giá.

Việc vệ sinh dữ liệu thoạt nhìn có vẻ dễ dàng. Giả sử chúng ta đang xoay quanh một danh sách các tùy chọn, bash đã cung cấp một cách tuyệt vời để khử trùng các phần tử riêng lẻ và một cách khác để khử trùng toàn bộ mảng dưới dạng một chuỗi đơn:

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

Bây giờ giả sử chúng ta muốn thêm một tùy chọn để chuyển hướng đầu ra làm đối số cho println. Tất nhiên, chúng tôi có thể chuyển hướng đầu ra của println trên mỗi cuộc gọi, nhưng vì lợi ích của ví dụ, chúng tôi sẽ không làm điều đó. Chúng tôi sẽ cần sử dụng eval, vì các biến không thể được sử dụng để chuyển hướng đầu ra.

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.

Có vẻ tốt, phải không? Vấn đề là, eval phân tích cú pháp hai lần dòng lệnh (trong bất kỳ trình bao nào). Trong lần phân tích cú pháp đầu tiên, một lớp trích dẫn bị xóa. Khi loại bỏ dấu ngoặc kép, một số nội dung biến đổi sẽ được thực thi.

Chúng tôi có thể khắc phục điều này bằng cách cho phép mở rộng biến diễn ra trong eval. Tất cả những gì chúng ta phải làm là trích dẫn đơn mọi thứ, để nguyên dấu ngoặc kép. Một ngoại lệ: chúng tôi phải mở rộng chuyển hướng trước eval, vì vậy điều đó phải nằm ngoài dấu ngoặc kép:

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.

Điều này sẽ hoạt động. Nó cũng an toàn miễn là $1trong printlnkhông bao giờ bẩn.

Bây giờ, hãy chờ một chút: Tôi sử dụng cùng một cú pháp không được trích dẫn mà chúng ta đã sử dụng ban đầu sudo! Tại sao nó hoạt động ở đó mà không phải ở đây? Tại sao chúng ta phải trích dẫn duy nhất mọi thứ? sudohiện đại hơn một chút: nó biết đặt trong dấu ngoặc kép mỗi đối số mà nó nhận được, mặc dù đó là sự đơn giản hóa quá mức. evalchỉ cần nối mọi thứ.

Thật không may, không có trình thay thế thả vào evalnào xử lý các đối số như sudohiện tại, cũng như evalmột trình bao được tích hợp sẵn; điều này rất quan trọng, vì nó sử dụng môi trường và phạm vi của mã xung quanh khi nó thực thi, thay vì tạo một ngăn xếp và phạm vi mới như một hàm.

đánh giá các lựa chọn thay thế

Các trường hợp sử dụng cụ thể thường có các lựa chọn thay thế khả thi eval. Đây là một danh sách hữu ích. commandđại diện cho những gì bạn thường gửi đến eval; thay thế trong bất cứ điều gì bạn muốn.

Không ra đâu

Dấu hai chấm đơn giản là không chọn trong bash:

:

Tạo một vỏ con

( command )   # Standard notation

Thực thi đầu ra của một lệnh

Không bao giờ dựa vào lệnh bên ngoài. Bạn phải luôn kiểm soát được giá trị trả về. Đặt những điều này trên dòng riêng của họ:

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

Chuyển hướng dựa trên biến

Trong mã gọi điện, ánh xạ &3(hoặc bất kỳ thứ gì cao hơn &2) tới mục tiêu của bạn:

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!

Nếu đó là cuộc gọi một lần, bạn sẽ không phải chuyển hướng toàn bộ shell:

func arg1 arg2 3>&2

Trong hàm đang được gọi, hãy chuyển hướng đến &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!

Biến hướng

Tình huống:

VAR='1 2 3'
REF=VAR

Xấu:

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

Tại sao? Nếu REF chứa một dấu ngoặc kép, điều này sẽ phá vỡ và mở mã để khai thác. Có thể khử trùng REF, nhưng thật lãng phí thời gian khi bạn có điều này:

echo "${!REF}"

Đúng vậy, bash đã tích hợp sẵn tính năng định hướng thay đổi kể từ phiên bản 2. Nó sẽ phức tạp hơn một chút so với việc evalbạn muốn làm điều gì đó phức tạp hơn:

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

Bất kể, phương pháp mới trực quan hơn, mặc dù nó có vẻ không giống như vậy đối với những người đã từng lập trình có kinh nghiệm eval.

Mảng liên kết

Các mảng liên kết được triển khai thực chất trong bash 4. Một lưu ý: chúng phải được tạo bằng cách sử dụng 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

Trong các phiên bản cũ hơn của bash, bạn có thể sử dụng hướng biến:

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
Tôi đang thiếu một đề cập đến eval "export $var='$val'"... (?)
Zrin

1
@Zrin Rất có thể điều đó không làm được những gì bạn mong đợi. export "$var"="$val"có lẽ là những gì bạn muốn. Lần duy nhất bạn có thể sử dụng biểu mẫu của mình là nếu var='$var2'và bạn muốn tham khảo gấp đôi nó - nhưng bạn không nên cố gắng làm bất cứ điều gì như vậy trong bash. Nếu bạn thực sự phải, bạn có thể sử dụng export "${!var}"="$val".
Zenexer

1
@anishsane: Đối với giả sửx="echo hello world";xeval $x$($x) của bạn , Sau đó, để thực thi bất cứ điều gì được chứa trong đó , chúng ta có thể sử dụng Tuy nhiên, sai rồi phải không? Đúng: $($x)là sai vì nó chạy echo hello worldvà sau đó cố gắng chạy đầu ra đã chụp (ít nhất là trong bối cảnh mà tôi nghĩ bạn đang sử dụng nó), điều này sẽ thất bại trừ khi bạn có một chương trình được gọi là hellokick xung quanh.
Jonathan Leffler

1
@tmow Ah, vì vậy bạn thực sự muốn có chức năng eval. Nếu đó là những gì bạn muốn, thì bạn có thể sử dụng eval; chỉ cần lưu ý rằng nó có rất nhiều cảnh báo bảo mật. Đó cũng là một dấu hiệu cho thấy có một lỗ hổng thiết kế trong ứng dụng của bạn.
Zenexer

1
ref="${REF}_2" echo "${!ref}"ví dụ sai, nó sẽ không hoạt động như dự định vì bash thay thế các biến trước khi lệnh được thực thi. Nếu refbiến thực sự không được xác định trước đó, kết quả của thay thế sẽ là ref="VAR_2" echo "", và đó là những gì sẽ được thực thi.
Yoory N.

17

Làm thế nào để làm cho evalan toàn

eval có thể được sử dụng một cách an toàn - nhưng tất cả các đối số của nó cần được trích dẫn trước. Đây là cách thực hiện:

Chức năng này sẽ làm điều đó cho bạn:

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

Ví dụ sử dụng:

Với một số thông tin người dùng nhập không đáng tin cậy:

% input="Trying to hack you; date"

Tạo một lệnh để đánh giá:

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

Đánh giá nó, với trích dẫn có vẻ đúng:

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

Lưu ý rằng bạn đã bị tấn công. dateđược thực thi hơn là được in theo nghĩa đen.

Thay vào đó bằng token_quote():

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

eval không phải là xấu - nó chỉ bị hiểu lầm :)


Hàm "token_quote" sử dụng các đối số của nó như thế nào? Tôi không thể tìm thấy bất kỳ tài liệu nào về tính năng này ...
Akito


Tôi đoán tôi đã nói nó quá rõ ràng. Ý tôi là các đối số hàm. Tại sao không có arg="$1"? Làm thế nào để vòng lặp for biết được các đối số nào đã được truyền cho hàm?
Akito

Tôi sẽ đi xa hơn là chỉ đơn giản là "hiểu lầm", nó cũng thường bị sử dụng sai và thực sự không cần thiết. Câu trả lời của Zenexer đề cập đến rất nhiều trường hợp như vậy nhưng bất kỳ việc sử dụng nào evalcũng phải là một dấu hiệu đỏ và được kiểm tra chặt chẽ để xác nhận rằng thực sự không có lựa chọn nào tốt hơn do ngôn ngữ này cung cấp.
dimo414
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.