bashshellshock-bash-bug

Strange Bash function export for the Shellshock bug


Why does the code

date
bash -c "date"
declare -x date='() { echo today; }' #aka export date='() { echo today; }'
date
bash -c "date"

print

Wed Sep 24 22:01:50 CEST 2014
Wed Sep 24 22:01:50 CEST 2014
Wed Sep 24 22:01:50 CEST 2014
today

?

Where (and why) does the evaluation

 date$date

happen and getting

 date() {echo today; }

Ad: @Etan Reisner

  1. I exporting a variable - not a function. Bash makes a function from it. The
export date='someting'

is still a variable regardless of its content. So, why is

export date='() { echo something; }' #Note, it is a variable, not function.

converted to an function?

  1. The mentioned security advisory talks about the execution of the command following the variable, for example,
x='() { echo I do nothing; }; echo vulnerable' bash -c ':'
                              ^^^^^^^^^^^^^^^
                              This is executed - this vunerability is CLOSED in version 4.3.25(1).

The command after the env-definition isn't executed in the latest Bash.

But the question remains - Why does Bash convert the exported variable to a function?

It is a bug ;) Full demo, based on @chepner's answer:

#Define three variables
foo='() { echo variable foo; }'    # ()crafted
qux='() { echo variable qux; }'    # ()crafted
bar='variable bar'                 # Normal
export foo qux bar                 # Export

#Define the same name functions (but not qux!)
foo() { echo "function foo"; }
bar() { echo "function bar"; }
declare -fx foo bar                 #Export

#printouts
echo "current shell foo variable:=$foo="
echo "current shell foo function:=$(foo)="
echo "current shell bar variable:=$bar="
echo "current shell bar function:=$(bar)="
echo "current shell qux variable:=$qux="
echo "current shell qux function:=$(qux)="

#subshell
bash -c 'echo subshell foo variable:=$foo='
bash -c 'echo subshell foo command :=$(foo)='
bash -c 'echo subshell bar variable:=$bar='
bash -c 'echo subshell bar command :=$(bar)='
bash -c 'echo subshell qux variable:=$qux='
bash -c 'echo subshell qux command :=$(qux)='

prints

current shell foo variable:=() { echo variable foo; }=
current shell foo function:=function foo=
current shell bar variable:=variable bar=
current shell bar function:=function bar=
current shell qux variable:=() { echo variable qux; }=
tt: line 20: qux: command not found
current shell qux function:==
subshell foo variable:==                   #<-- LOST the exported foo variable
subshell foo command :=function foo=
subshell bar variable:=variable bar=
subshell bar command :=function bar=
subshell qux variable:==                   #<-- And the variable qux got converted to
subshell qux command :=variable qux=       #<-- function qux in the subshell (!!!).

Avoiding the long comments, here is code from the Bash sources:

 if (privmode == 0 && read_but_dont_execute == 0 && STREQN ("() {", string, 4))
                                                           ^^^^^^^^ THE PROBLEM
    {
      string_length = strlen (string);
      temp_string = (char *)xmalloc (3 + string_length + char_index);

      strcpy (temp_string, name);
      temp_string[char_index] = ' ';
      strcpy (temp_string + char_index + 1, string);

      if (posixly_correct == 0 || legal_identifier (name))
        parse_and_execute (temp_string, name, SEVAL_NONINT|SEVAL_NOHIST);

      /* Ancient backwards compatibility.  Old versions of bash exported
         functions like name()=() {...} */

The "ancient" (seems) was better... :)

      if (name[char_index - 1] == ')' && name[char_index - 2] == '(')
        name[char_index - 2] = '\0';

Solution

  • The key point to remember is that

    foo='() { echo 5; }'
    

    only defines a string parameter with a string that looks a lot like a function. It's still a regular string:

    $ echo $foo
    () { echo 5; }
    

    And not a function:

    $ foo
    bash: foo: command not found
    

    Once foo is marked for export,

    $ export foo
    

    any child Bash will see the following string in its environment:

    foo=() { echo 5; }
    

    Normally, such strings become shell variables, using the part preceding the = as the name and the part following the value. However, Bash treats such strings specially by defining a function instead:

    $ echo $foo
    
    $ foo
    5
    

    You can see that the environment itself is not changed by examining it with something other than Bash:

    $ perl -e 'print $ENV{foo}\n"'
    () { echo 5
    }
    

    (The parent Bash replaces the semicolon with a newline when creating the child's environment, apparently). It's only the child Bash that creates a function instead of a shell variable from such a string.

    The fact that foo could be both a parameter and a function within the same shell;

    $ foo=5
    $ foo () { echo 9; }
    $ echo $foo
    5
    $ foo
    9
    

    explains why -f is needed with export. export foo would cause the string foo=5 to be added to the environment of a child; export -f foo is used to add the string foo=() { echo 9; }.