11
\$\begingroup\$

Note: If you googled this by the title of this question, don't use this script unless you know what it is supposed to do.

This is a script in bash 3+ that I have used for long for preventing rm * and rm -rf * from accidentally invoked and removing important files by mistake. I put it in my ~/.bash_aliases.

alias rm='set -f;rm'
rm(){
    if [[ "$-" == *i* ]]
    then
        if [ "$1" = "*" ] || [ "$2" = "*" ] || [ "$1" = "./*" ] || [ "$2" = "./*" ]
        then
            echo "Abort: refusing to remove *, please go to the parent folder and do rm <folder_name>/*" 1>&2
            set +f
            return 1
        fi
    fi  
    set +f
    /bin/rm -i $@
}

set +f

I would like to know whether there are any vulnerabilities and whether it can be improved.

\$\endgroup\$

5 Answers 5

12
\$\begingroup\$

The vulnerability I see is that only the first two arguments are checked. You could check all of them:

rm() { 
    [[ $- == *i* ]] && for arg
    do 
        if [[ $arg = "*" || $arg = "./*" ]]
        then
            # abort
       fi
   done
   # do the rm

This kind of checking will miss other dangerous wildcards like ** or ?*. You can get safer checking by expanding * yourself, then see if the expanded arguments contain that same list of files:

# alias not needed here; we want globs to be expanded
rm() {
    declare -a star=(*)
    declare -a dotslashstar=(./*)
    if [[ "$@" == *"${star[@]}"*  ||  "$@" == *"${dotslashstar[@]}"* ]]
    then
         # abort

... but then you can't (for example) empty a directory full of temp files with rm *.tmp, if *.tmp and * match the same thing.

\$\endgroup\$
1
  • 2
    \$\begingroup\$ I love your idea of expanding * myself. \$\endgroup\$ Commented Mar 9, 2019 at 3:20
8
\$\begingroup\$

Double-quote variables used in command parameters

This is a bug:

/bin/rm -i $@

What will happen if you try to delete file a b (with space in the name)? Most likely this:

rm: a: No such file or directory
rm: b: No such file or directory

Always write "$@" instead of unquoted $@.

Unfortunately, as you pointed out in a comment, this will cause another problem: arguments containing globs will be taken literally. In short, it's difficult to have the cake and eat it too.

You could mitigate the problem by looping over the arguments, and if you detect a glob, then expand it yourself:

for arg; do
    if [[ $arg == *[*?]* ]]; then
        expanded=($arg)
        echo rm -i "${expanded[@]}"
    else
        echo rm -i "$arg"
    fi
done

This still won't be perfect, because it doesn't handle the case when an argument contains both spaces and globs. A robust solution would take more effort, and not worth doing in Bash. (See this example delegating the hard work to Python.)

Use command to bypass aliases

Don't worry about the absolute path of commands. Use command to bypass aliases:

command rm -i "$@"

Redundant file descriptors

In echo "Abort: ..." 1>&2, the file descriptor 1 is redundant, you can safely omit it.

Preserving the user's environment

This is a minor nitpick. When the alias is executed, it will do set +f, regardless of whatever was the original setting in the shell, which may not be the same. This is really just a minor nitpick, for the record. I wouldn't care about this tiny impractical detail either.

\$\endgroup\$
2
  • 1
    \$\begingroup\$ I had an issue with patterns containing asterisk (e.g. rm -f .*.swp) and that's why I didn't quote $@. Your other advices are taken. \$\endgroup\$ Commented Mar 9, 2019 at 15:34
  • 1
    \$\begingroup\$ @WeijunZhou That's a good point. See my updated answer. \$\endgroup\$
    – janos
    Commented Mar 10, 2019 at 7:21
4
\$\begingroup\$

Don't call your function rm

If you start depending on this safety net, you'll eventually have an accident on a system with a standard rm (e.g. when you become root and find yourself using dash for admin tasks).

I'd suggest

weijun_rm() {
    # your safer implementation
}

rm() {
     echo "Disabled - please use weijun_rm instead" >&2
     return 1
}

This will train you not to use rm for deleting files. When your function isn't available and you have to use the real rm, you will be on alert and extra careful to check the arguments.

\$\endgroup\$
2
  • \$\begingroup\$ Thank you for your suggestion. This is just an extra protection. I don't depend on this and I know what I am doing in most cases. Just in case I didn't get enough sleep and do sth silly ... \$\endgroup\$ Commented Mar 11, 2019 at 14:06
  • 1
    \$\begingroup\$ I guess anyone can accidentally brush Enter when typing rm *~ or rm *.o or the like (I normally write echo, and only type the rm when I'm happy with the rest). But perhaps my advice might be useful for others who aren't so disciplined. \$\endgroup\$ Commented Mar 11, 2019 at 14:45
1
\$\begingroup\$

Restore options properly

Instead of set -f in our alias and unconditional set +f in the function, we can save the current shell options using set +o to print them as commands:

fun() {
    restore=$(set +o)

    set -f        
    # ... and any other option changes
    # and perform the task

    # finally (instead of set +f)
    eval "$restore"
}
\$\endgroup\$
1
\$\begingroup\$

This is the version I am using after learning from other answers. Only the core if is written.

if [[ $- == *i* ]]
then
    set +f
    for arg in "$@"
    do # We want to abort the whole command even if only some of the arguments contain dangerous patterns
        if [[ "$arg" == "*" || "$arg" == "./*" ]] # Can be replaced by a custom program written in Python etc. to detect dangerous patterns
        then
            echo "Abort: refusing to remove *, please go to the parent folder and do rm <folder_name>/*" 1>&2
            set +f
            return 1
        fi
    done
    declare -a all
    for arg in "$@"
    do #We need to collect all arguments instead of `rm`-ing one by one otherwise `rm -rf file` does not work properly
        if [[ "$arg" == *[*?]* ]]
        then
            IFS= expanded=($arg)
            all+=("${expanded[@]}")
        else
            all+=("$arg")                                                                                                                                                                               
        fi
    done
    command rm -i "${all[@]}"
...
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.