Bash – How to explicitly and safely force the use of a built-in command in bash

bashSecurity

There's a similar question that deals with the 'wrapping' scenario, where you want to replace for example cd with a command that calls the builtin cd.

However, in light of shellshock et al and knowing that bash imports functions from the environment, I've done a few tests and I can't find a way to safely call the builtin cd from my script.

Consider this

cd() { echo "muahaha"; }
export -f cd

Any scripts called in this environment using cd will break (consider the effects of something like cd dir && rm -rf .).

There are commands to check the type of a command (conveniently called type) and commands for executing the builtin version rather than a function (builtin and command). But, lo and behold, these can be overridden using functions as well

builtin() { "$@"; }
command() { "$@"; }
type() { echo "$1 is a shell builtin"; }

Will yield the following:

$ type cd
cd is a shell builtin
$ cd x
muahaha
$ builtin cd x
muahaha
$ command cd x
muahaha

Is there any way to safely force bash to use the builtin command, or at least detect that a command isn't a builtin, without clearing the entire environment?

I realize if someone controls your environment you're probably screwed anyway, but at least for aliases you've got the option to not call the alias by inserting a \ before it.

Best Answer

Olivier D is almost correct, but you must set POSIXLY_CORRECT=1 before running unset. POSIX has a notion of Special Built-ins, and bash supports this. unset is one such builtin. Search for SPECIAL_BUILTIN in builtins/*.c in the bash source for a list, it includes set, unset, export, eval and source.

$ unset() { echo muahaha-unset; }
$ unset unset
muahaha-unset
$ POSIXLY_CORRECT=1
$ unset unset

The rogue unset has now been removed from the environment, if you unset command, type, builtin then you should be able to proceed, but unset POSIXLY_CORRECT if you are relying on non-POSIX behaviour or advanced bash features.

This does not address aliases though, so you must use \unset to be sure it works in interactive shell (or always, in case expand_aliases is in effect).

For the paranoid, this should fix everything, I think:

POSIXLY_CORRECT=1
\unset -f help read unset
\unset POSIXLY_CORRECT
re='^([a-z:.\[]+):' # =~ is troublesome to escape
while \read cmd; do 
    [[ "$cmd" =~ $re ]] && \unset -f ${BASH_REMATCH[1]}; 
done < <( \help -s "*" )

(while, do, done and [[ are reserved words and don't need precautions.) Note we are using unset -f to be sure to unset functions, although variables and functions share the same namespace it's possible for both to exist simultaneously (thanks to Etan Reisner) in which case unset-ing twice would also do the trick. You can mark a function readonly, bash does not prevent you unsetting a readonly function up to and including bash-4.2, bash-4.3 does prevent you but it still honours the special builtins when POSIXLY_CORRECT is set.

A readonly POSIXLY_CORRECT is not a real problem, this is not a boolean or flag its presence enables POSIX mode, so if it exists as a readonly you can rely on POSIX features, even if the value is empty or 0. You'll simply need to unset problematic functions a different way than above, perhaps with some cut-and-paste:

\help -s "*" | while IFS=": " read cmd junk; do echo \\unset -f $cmd; done

(and ignore any errors) or engage in some other scriptobatics.


Other notes:

  • function is a reserved word, it can be aliased but not overridden with a function. (Aliasing function is mildly troublesome because \function is not acceptable as a way of bypassing it)
  • [[, ]] are reserved words, they can be aliased (which will be ignored) but not overridden with a function (though functions can be so named)
  • (( is not a valid name for a function, nor an alias