Warum sollte eval in Bash vermieden werden und was sollte ich stattdessen verwenden?

107

Immer wieder sehe ich Bash-Antworten auf Stack Overflow mit evalund die Antworten werden für die Verwendung eines solchen "bösen" Konstrukts geschlagen, Wortspiel beabsichtigt. Warum ist evalso böse?

evalWas kann ich stattdessen verwenden, wenn es nicht sicher verwendet werden kann?

Zenexer
quelle

Antworten:

148

Dieses Problem hat mehr zu bieten als man denkt. Wir beginnen mit dem Offensichtlichen: evalHat das Potenzial, "schmutzige" Daten auszuführen. Schmutzige Daten sind alle Daten, die nicht als sicher für die Verwendung in der Situation XYZ umgeschrieben wurden. In unserem Fall handelt es sich um eine Zeichenfolge, die nicht so formatiert wurde, dass sie für die Auswertung sicher ist.

Das Desinfizieren von Daten erscheint auf den ersten Blick einfach. Angenommen, wir werfen eine Liste von Optionen durch, bietet bash bereits eine großartige Möglichkeit, einzelne Elemente zu bereinigen, und eine andere Möglichkeit, das gesamte Array als einzelne Zeichenfolge zu bereinigen:

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

Angenommen, wir möchten eine Option zum Umleiten der Ausgabe als Argument für println hinzufügen. Wir könnten natürlich nur die Ausgabe von println bei jedem Aufruf umleiten, aber zum Beispiel werden wir das nicht tun. Wir müssen verwenden eval, da Variablen nicht zum Umleiten der Ausgabe verwendet werden können.

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.

Sieht gut aus, oder? Das Problem ist, dass eval zweimal die Befehlszeile analysiert (in jeder Shell). Beim ersten Durchlauf des Parsens wird eine Zitierschicht entfernt. Wenn Anführungszeichen entfernt werden, werden einige variable Inhalte ausgeführt.

Wir können dies beheben, indem wir die variable Erweiterung innerhalb der eval. Alles, was wir tun müssen, ist, alles in einfache Anführungszeichen zu setzen und die doppelten Anführungszeichen dort zu belassen, wo sie sind. Eine Ausnahme: Wir müssen die Umleitung vorher erweitern eval, damit sie außerhalb der Anführungszeichen bleibt:

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.

Das sollte funktionieren. Es ist auch sicher, solange $1in printlnnie verschmutzt ist.

Warten Sie einen Moment: Ich verwende dieselbe nicht zitierte Syntax, die wir ursprünglich sudoimmer verwendet haben! Warum funktioniert es dort und nicht hier? Warum mussten wir alles in Anführungszeichen setzen? sudoist etwas moderner: Es kann jedes Argument, das es erhält, in Anführungszeichen setzen, obwohl dies eine übermäßige Vereinfachung darstellt. evalverkettet einfach alles.

Leider gibt es keinen Drop-In-Ersatz dafür eval, der Argumente so behandelt wie dies der sudoFall ist, ebenso evalwie eine integrierte Shell. Dies ist wichtig, da es bei der Ausführung die Umgebung und den Umfang des umgebenden Codes übernimmt, anstatt wie eine Funktion einen neuen Stapel und Bereich zu erstellen.

eval Alternativen

Bestimmte Anwendungsfälle bieten häufig praktikable Alternativen zu eval. Hier ist eine praktische Liste. commandstellt dar, an was Sie normalerweise senden würden eval; Ersatz in was auch immer Sie bitte.

No-op

Ein einfacher Doppelpunkt ist ein No-Op in Bash:

:

Erstellen Sie eine Sub-Shell

( command )   # Standard notation

Führen Sie die Ausgabe eines Befehls aus

Verlassen Sie sich niemals auf einen externen Befehl. Sie sollten immer die Kontrolle über den Rückgabewert haben. Setzen Sie diese auf ihre eigenen Zeilen:

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

Umleitung basierend auf Variable

Wenn Sie Code aufrufen, ordnen Sie Ihrem Ziel Folgendes zu &3(oder etwas höheres als &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!

Wenn es sich um einen einmaligen Anruf handeln würde, müssten Sie nicht die gesamte Shell umleiten:

func arg1 arg2 3>&2

Leiten Sie innerhalb der aufgerufenen Funktion zu &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 Indirektion

Szenario:

VAR='1 2 3'
REF=VAR

Schlecht:

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

Warum? Wenn REF ein doppeltes Anführungszeichen enthält, wird der Code unterbrochen und für Exploits geöffnet. Es ist möglich, REF zu bereinigen, aber es ist Zeitverschwendung, wenn Sie Folgendes haben:

echo "${!REF}"

Richtig, in Bash ist ab Version 2 eine variable Indirektion integriert. Es wird etwas schwieriger, als evalwenn Sie etwas Komplexeres tun möchten:

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

Unabhängig davon ist die neue Methode intuitiver, obwohl sie erfahrenen Programmierern, die es gewohnt sind, möglicherweise nicht so erscheint eval.

Assoziative Arrays

Assoziative Arrays sind in Bash 4 intrinsisch implementiert. Eine Einschränkung: Sie müssen mit erstellt werden 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 älteren Versionen von bash können Sie die variable Indirektion verwenden:

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"`"
# ...
Zenexer
quelle
4
Ich vermisse eine Erwähnung von eval "export $var='$val'"... (?)
Zrin
1
@ Zrin Wahrscheinlich macht das nicht das, was Sie erwarten. export "$var"="$val"ist wahrscheinlich was du willst. Das einzige Mal, wenn Sie Ihr Formular verwenden, ist if var='$var2', und Sie möchten es doppelt dereferenzieren - aber Sie sollten nicht versuchen, so etwas in bash zu tun. Wenn Sie wirklich müssen, können Sie verwenden export "${!var}"="$val".
Zenexer
1
@anishsane: Für Ihre Annahme , x="echo hello world";dann, um auszuführen, was auch immer in enthalten ist x, können wir verwenden.eval $x$($x) Ist jedoch falsch, nicht wahr ? Ja, $($x)ist falsch, weil es ausgeführt wird echo hello worldund dann versucht, die erfasste Ausgabe auszuführen (zumindest in den Kontexten, in denen Sie es meiner Meinung nach verwenden). Dies schlägt fehl, es sei denn, Sie haben ein Programm namens " helloKicking Around".
Jonathan Leffler
1
@tmow Ah, also willst du eigentlich eval Funktionalität. Wenn Sie dies möchten, können Sie eval verwenden. Denken Sie daran, dass es viele Sicherheitsvorkehrungen gibt. Dies ist auch ein Zeichen dafür, dass Ihre Anwendung einen Konstruktionsfehler aufweist.
Zenexer
1
ref="${REF}_2" echo "${!ref}"Beispiel ist falsch, es funktioniert nicht wie beabsichtigt, da Bash Variablen ersetzt, bevor ein Befehl ausgeführt wird. Wenn die refVariable zuvor wirklich undefiniert ist, ist das Ergebnis der Substitution ref="VAR_2" echo "", und das wird ausgeführt.
Yoory N.
17

Wie man evalsicher macht

eval kann sicher verwendet werden - aber alle seine Argumente müssen zuerst zitiert werden. Hier ist wie:

Diese Funktion erledigt das für Sie:

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

Anwendungsbeispiel:

Bei einigen nicht vertrauenswürdigen Benutzereingaben:

% input="Trying to hack you; date"

Erstellen Sie einen Befehl zum Auswerten:

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

Bewerten Sie es mit scheinbar korrektem Zitat:

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

Beachten Sie, dass Sie gehackt wurden. datewurde ausgeführt, anstatt wörtlich gedruckt zu werden.

Stattdessen mit token_quote():

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

eval ist nicht böse - es wird nur missverstanden :)

Tom Hale
quelle
Wie verwendet die Funktion "token_quote" ihre Argumente? Ich kann keine Dokumentation zu dieser Funktion finden ...
Akito
Ich denke, ich habe es zu unklar formuliert. Ich meinte die Funktionsargumente. Warum gibt es keine arg="$1"? Woher weiß die for-Schleife, welche Argumente an die Funktion übergeben wurden?
Akito
Ich würde weiter gehen als nur "missverstanden", es wird auch oft missbraucht und wirklich nicht benötigt. Die Antwort von Zenexer deckt viele solcher Fälle ab, aber jede Verwendung evalsollte eine rote Fahne sein und genau untersucht werden, um zu bestätigen, dass es wirklich keine bessere Option gibt, die die Sprache bereits bietet.
dimo414