Bash에서 eval을 피해야하는 이유는 무엇이며 대신 무엇을 사용해야합니까?


107

몇 번이고 Stack Overflow에 대한 Bash 답변 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이 모든 셸에서 명령 줄을 두 번 구문 분석한다는 것입니다. 구문 분석의 첫 번째 단계에서 인용의 한 계층이 제거됩니다. 따옴표가 제거되면 일부 가변 내용이 실행됩니다.

.NET Framework 내에서 변수 확장이 발생하도록하여이 문제를 해결할 수 있습니다 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내장 쉘입니다; 이는 함수처럼 새로운 스택과 범위를 생성하는 것보다 실행될 때 주변 코드의 환경과 범위를 차지하므로 중요합니다.

평가 대안

특정 사용 사례에는 종종 eval. 다음은 편리한 목록입니다. command일반적으로 보낼 대상을 나타냅니다 eval. 원하는대로 대체하십시오.

작동하지 않음

간단한 콜론은 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.

변수에 따른 리디렉션

호출 코드에서 대상에 매핑 &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'"... (?)
Zrin

1
@Zrin 가능성은 당신이 기대하는 것을하지 않습니다. export "$var"="$val"아마도 당신이 원하는 것입니다. 양식을 사용할 수있는 유일한 시간은 if var='$var2'이고 이중 역 참조를 원하지만 bash에서 이와 같은 작업을해서는 안됩니다. 꼭 필요한 경우 export "${!var}"="$val".
Zenexer 2015 년

1
@anishsane : 당신의 가정 x="echo hello world";을 위해 , 그러면에 포함 된 것을 실행하기 위해 x, 우리는 사용할 수 있습니다. eval $x그러나, $($x)틀 렸습니다. 예 : $($x)실행 echo hello world한 다음 캡처 된 출력을 실행하려고 시도하기 때문에 잘못되었습니다 (적어도 사용하고 있다고 생각하는 컨텍스트에서) hello.
조나단 레플러

1
@tmow 아, 그래서 당신은 실제로 평가 기능을 원합니다. 그것이 당신이 원하는 것이라면 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"

평가할 명령을 생성합니다.

% 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 루프는 어떤 인수가 함수에 전달되었는지 어떻게 알 수 있습니까?
Akito

나는 단순히 "오해"하는 것 이상으로 나아가고 , 또한 종종 오용 되고 실제로 필요하지 않습니다. Zenexer의 답변은 이러한 많은 경우를 다루지 만 사용은 위험 신호 eval이며 언어에서 이미 제공하는 더 나은 옵션이 실제로 있는지 확인하기 위해 면밀히 조사되어야합니다.
dimo414
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.