Collapsed PLUG
Collapsed PLUG

Reputation: 304

Spaces inside brace expansion led to weird behavior

A few days ago, my lab's server suffered a serious meltdown when one of our interns accidentally copy-pasted this code on bash trying to delete node.js.

$ rm -rfv /usr/{bin/node,lib / node,share / man /* / node.*};

They tried a brace expansion to list up directories to delete, but notice spaces between the directory separators (/). This ended up deleting everything on our server because they applied sudo. I tried this command on my virtual machine and confirmed that it was pretty much equivalent to rm -rf /.

I'm confused about the way bash interpreted the statement. When I try to create a simple, nondestructive command that does similar things (spaces working as separators for expansion,) I don't seem to be able to pull that off. I tried the first command but with for loop in bash:

$ for f in /usr/{bin/node,lib / node,share / man /* / node.*}; do echo $f; done

which listed some contents in node.js and the directories in /. This should confirm that this is not a special feature in rm.

But when I try something like this:

$ for f in {a,b c,d e}; echo $f

It results in a syntax error near echo where I expected a to e, each letter in a single line.

I did some research, but I couldn't find anything that explains this behavior. Can someone please tell me, in the first command, how did bash interpret this command?


p.s. I found out that in zsh the 'for loop test' version doesn't work. Never tried the real 'rm test' though. I'm scared.

Upvotes: 3

Views: 254

Answers (1)

mklement0
mklement0

Reputation: 440536

You must \-escape all spaces that are part of your brace expansion:

$ printf '%s\n' {a,b\ c}
a
b c

Brace expansions only work:

  • when they're neither neither single- nor double-quoted (you got that part right)

  • and when they're recognized as a single word (token) by the shell (that's where your attempt fell short) - hence the need for character-individual quoting of spaces with \.

Without this, bash breaks what you meant to be a brace expansion into multiple arguments, and brace expansion never happens - see below.


As for how bash parses /usr/{bin/node,lib / node,share / man /* / node.*}: The following tells you the resulting arguments (with actual globbing omitted to better demonstrate what happens):

$1=/usr/{bin/node,lib
$2=/
$3=node,share
$4=/
$5=man
$6=/*
$7=/
$8=node.*}

As you can see:

  • The unescaped spaces caused the word-splitting to occur by spaces, breaking what you meant to be brace expressions into multiple arguments.

  • One of the resulting arguments /*, unfortunately, matches all (non-hidden) items in the root directory, and therefore wreaks havoc when passed to sudo rm.

Upvotes: 5

Related Questions