1. Overview

When storing a command into a variable, we usually encounter problems like whitespace characters and complex command constructions.

In this article, we’ll see how we can store a command into a variable and run it. We’ll also discuss what limitations these methods have and the problems we face when using alternatives like eval.

2. Storing the Command in an Array

We can use an array to store a single command and its parameters. We set the first element with the program’s name, and then each parameter in subsequent array positions:

$ COMMAND=("ls" "-l" "/var/log/httpd/access_log" "/var/log/file with spaces")

Then, we can execute the command by expanding all the elements of the array:

$ "${COMMAND[@]}"
-rw-r--r-- 1 root root 0 Jan  9 09:38 /var/log/file with spaces
-rw-r--r-- 1 root root 0 Jan  8 17:21 /var/log/httpd/access_log

Note that we should use quotes around the array to correctly handle whitespace.

We can change the command or its parameters after the array is defined:

$ COMMAND=("cat" "/var/log/messages")
$ echo ${COMMAND[@]}
cat /var/log/messages
$ COMMAND[0]="less"
$ echo ${COMMAND[@]}
less /var/log/messages
$ COMMAND[1]="/var/log/httpd/access_log"
$ echo ${COMMAND[@]}
less /var/log/httpd/access_log

Also, we can build the array using variables:

$ PROGRAM="ls"
$ PARAMETER="-l"
$ FILE1="/var/log/httpd/access_log"
$ FILE2="/var/log/file with spaces"
$ COMMAND=("$PROGRAM" "$PARAMETER" "$FILE1" "$FILE2")
$ "${COMMAND[@]}"
-rw-r--r-- 1 root root 0 Jan  9 09:38 /var/log/file with spaces
-rw-r--r-- 1 root root 0 Jan  8 17:21 /var/log/httpd/access_log

And alternatively, we can separate the command from its parameters by storing the command in a variable and the parameters in an array:

$ COMMAND="cat"
$ PARAMETERS=("/var/log/httpd/access_log", "/var/log/httpd/access_log.1")
$ "$COMMAND" "${PARAMETERS[@]}"

Now, we can use tac instead of cat by simply setting COMMAND=”tac”.

This method only works when we have a simple command and its parameters. It won’t work with redirections, pipes, or reserved words like if and while.

If we want to execute a complex command construction, then we should define a function instead.

3. Problems Storing Code in a Variable

When we store commands in a variable, we should take into account that variables were created to store data, not code.

This means that we’ll face problems when dealing with more complex cases.

When we store some command in a variable, the bash interpreter will only expand it and execute it as a simple command with its parameters. So, there are things that won’t work. Some of the most important are:

In short, bash will only expand the variables without interpreting their contents. There are some exceptions like filename expansions, but in general, we can’t store any arbitrary script in a variable.

We can test whether we can use more complex commands inside a variable. Let’s first try using pipes:

$ COMMAND=("stat" "/var/log/httpd/access_log" "|" "grep" "Modify")
$ ${COMMAND[@]}
  File: /var/log/httpd/access_log
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: fd02h/64770d    Inode: 3148361     Links: 1
Access: (0644/-rw-r--r--)  Uid: (    0/    root)   Gid: (    0/    root)
Access: 2021-01-08 17:21:23.031001506 -0300
Modify: 2021-01-08 17:21:23.031001506 -0300
Change: 2021-01-08 17:21:23.031001506 -0300
 Birth: 2021-01-08 17:21:23.031001506 -0300
stat: cannot statx '|': No such file or directory
stat: cannot statx 'grep': No such file or directory
stat: cannot statx 'Modify': No such file or directory

It didn’t work as expected, as we can see the pipe was passed as an argument to stat.

Now, let’s try using a variable inside the array:

$ FILE="/var/log/httpd/access_log" 
$ COMMAND=('ls' '-l' '$FILE')
$ ${COMMAND[@]}
ls: cannot access '$FILE': No such file or directory

When we consider storing a complex command in a variable as a solution, we should remember that this is a bad practice, and instead, we should just write a function.

Another common approach is to use eval to work around the problems mentioned before. In the next section, we’ll explore some security issues with this approach.

4. Problems Using eval

One way of dealing with complex cases is by using eval. There are other similar alternatives, like echo-ing the content of the variable and piping it to a shell input. All of them have the same problem.

eval expands its arguments, and then, the bash interpreter will execute them. So, when we use eval, we work around the problems mentioned in the previous section. However, this method of executing commands is potentially insecure.

When we use eval, there are no checks of any nature on the code we execute, so we’re opening the door to malicious code. This is especially a concern when our script is working with external inputs. Let’s make use of eval in an example:

$ test_eval() {
    COMMAND="stat"
    if [ "$DEREFERENCE" == "yes" ]; then
        COMMAND="$COMMAND -L"
    fi
    COMMAND=$COMMAND\ /home/backup/db_*
    if [ "$REDIRECT" == "yes" ]; then
        COMMAND=$COMMAND\ " > ${REDIRECT_OUTPUT:-/tmp/stat_output}"
    fi
    eval $COMMAND
}

In this script, we wanted to run stat on some files, optionally adding -L and/or redirecting the output. Let’s see what potential security issues we have:

  • There can be a malicious file in /home/backup
  • The variable REDIRECT_OUTPUT can contain malicious code

Let’s generate a file called /home/backup/db_;date and call test_eval():

$ touch '/home/backup/db_;date'
$ test_eval
/usr/bin/stat: cannot statx '/home/backup/db_': No such file or directory
Sun Jan 10 17:09:55 2021

As we can see, the semicolon broke the command, and the date command was executed.

Now, we can follow a similar idea with REDIRECT_OUTPUT:

$ REDIRECT=yes
$ REDIRECT_OUTPUT='/dev/null; date'
$ test_eval
Sun Jan 10 17:14:34 2021

Again, date was executed, and we can surely think of more dangerous examples.

5. Conclusion

In this article, we saw how we can use an array to store a single command and its parameters. Then, we discussed what problems and limitations we have when storing code in a variable.

Finally, we saw why we shouldn’t use eval as it makes our script vulnerable to malicious code.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.