Why should eval be avoided in Bash, and what should I use instead?

前端 未结 2 1982
傲寒
傲寒 2020-11-21 22:53

Time and time again, I see Bash answers on Stack Overflow using eval and the answers get bashed, pun intended, for the use of such an \"evil\" construct. Why is

相关标签:
2条回答
  • 2020-11-21 23:21

    There's more to this problem than meets the eye. We'll start with the obvious: eval has the potential to execute "dirty" data. Dirty data is any data that has not been rewritten as safe-for-use-in-situation-XYZ; in our case, it's any string that has not been formatted so as to be safe for evaluation.

    Sanitizing data appears easy at first glance. Assuming we're throwing around a list of options, bash already provides a great way to sanitize individual elements, and another way to sanitize the entire array as a single string:

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

    Now say we want to add an option to redirect output as an argument to println. We could, of course, just redirect the output of println on each call, but for the sake of example, we're not going to do that. We'll need to use eval, since variables can't be used to redirect output.

    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.
    

    Looks good, right? Problem is, eval parses twice the command line (in any shell). On the first pass of parsing one layer of quoting is removed. With quotes removed, some variable content gets executed.

    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 is 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"`"
    # ...
    
    0 讨论(0)
  • 2020-11-21 23:29

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

    0 讨论(0)
提交回复
热议问题