Bash에서 eval을 피해야하는 이유는 무엇이며 대신 무엇을 사용해야합니까?
몇 번이고 Stack Overflow에 대한 Bash 답변 eval
이 사용되는 것을보고 이러한 "사악한"구조를 사용하기 위해 답변이 부끄럽고 말장난을 당합니다. 왜 eval
그렇게 사악합니까?
eval
안전하게 사용할 수없는 경우 대신 무엇을 사용해야합니까?
이 문제에는 눈에 보이는 것보다 더 많은 것이 있습니다. 우리는 명백한 것부터 시작할 것입니다 : 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이 (모든 쉘에서) 명령 줄을 두 번 구문 분석한다는 것입니다. 구문 분석의 첫 번째 단계에서 인용의 한 계층이 제거됩니다. 따옴표가 제거되면 일부 가변 내용이 실행됩니다.
We can fix this by letting the variable expansion take place within the eval
. All we have to do is single-quote everything, leaving the double-quotes where they are. One exception: we have to expand the redirection prior to eval
, so that has to stay outside of the quotes:
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.
This should work. It's also safe as long as $1
in println
is never dirty.
Now hold on just a moment: I use that same unquoted syntax that we used originally with sudo
all of the time! Why does it work there, and not here? Why did we have to single-quote everything? sudo
is a bit more modern: it knows to enclose in quotes each argument that it receives, though that is an over-simplification. eval
simply concatenates everything.
Unfortunately, there is no drop-in replacement for eval
that treats arguments like sudo
does, as eval
is a shell built-in; this is important, as it takes on the environment and scope of the surrounding code when it executes, rather than creating a new stack and scope like a function does.
eval Alternatives
Specific use cases often have viable alternatives to eval
. Here's a handy list. command
represents what you would normally send to eval
; substitute in whatever you please.
No-op
A simple colon in a no-op in bash: :
Create a sub-shell
( command ) # Standard notation
Execute output of a command
Never rely on an external command. You should always be in control of the return value. Put these on their own lines:
$(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.
Redirection based on variable
In calling code, map &3
(or anything higher than &2
) to your target:
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!
If it were a one-time call, you wouldn't have to redirect the entire shell:
func arg1 arg2 3>&2
Within the function being called, redirect to &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!
Variable indirection
Scenario:
VAR='1 2 3'
REF=VAR
Bad:
eval "echo \"\$$REF\""
Why? If REF contains a double quote, this will break and open the code to exploits. It's possible to sanitize REF, but it's a waste of time when you have this:
echo "${!REF}"
That's right, bash has variable indirection built-in as of version 2. It gets a bit trickier than eval
if you want to do something more complex:
# 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\""
Regardless, the new method is more intuitive, though it might not seem that way to experienced programmed who are used to eval
.
Associative arrays
Associative arrays are implemented intrinsically in bash 4. One caveat: they must be created using 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
In older versions of bash, you can use variable indirection:
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"`"
# ...
How to make eval
safe
eval
can be safely used - but all of its arguments need to be quoted first. Here's how:
This function which will do it for you:
function token_quote {
local quoted=()
for token; do
quoted+=( "$(printf '%q' "$token")" )
done
printf '%s\n' "${quoted[*]}"
}
Example usage:
Given some untrusted user input:
% input="Trying to hack you; date"
Construct a command to eval:
% cmd=(echo "User gave:" "$input")
Eval it, with seemingly correct quoting:
% eval "$(echo "${cmd[@]}")"
User gave: Trying to hack you
Thu Sep 27 20:41:31 +07 2018
Note you were hacked. date
was executed rather than being printed literally.
Instead with token_quote()
:
% eval "$(token_quote "${cmd[@]}")"
User gave: Trying to hack you; date
%
eval
isn't evil - it's just misunderstood :)
What about
ls -la /path/to/foo | grep bar | bash
or
(ls -la /path/to/foo | grep bar) | bash
?
'developer tip' 카테고리의 다른 글
libstdc ++. so.6 : 공유 객체 파일을 열 수 없음 : 해당 파일 또는 디렉토리 없음 (0) | 2020.09.15 |
---|---|
Qt C ++ 집계 'std :: stringstream ss'에 불완전한 유형이 있으며 정의 할 수 없습니다. (0) | 2020.09.14 |
CMake를 사용하여 Visual Studio C ++ 프로젝트 파일 생성 (0) | 2020.09.14 |
Maven의 명령 줄 인수를 pom.xml의 속성으로 전달 (0) | 2020.09.14 |
PostgreSQL의 계산 / 계산 / 가상 / 파생 열 (0) | 2020.09.14 |