How to pass all arguments to a script called by /bin/bash

Solution 1:

tl;dr

sudo su cassandra -s /bin/bash -c \
   'exec /usr/local/cassandra/bin/nodetool \
    -u cassandra \
    -pwf /opt/apps/cassandra/resources/security/jmxremote.password \
    "$@"' \
   -- my-inner-shell -- "$@"

Or even

sudo su cassandra -s /usr/local/cassandra/bin/nodetool -- \
    -u cassandra \
    -pwf /opt/apps/cassandra/resources/security/jmxremote.password \
    "$@"

Analysis

At first let's explicitly state that in your commands sudo su runs su and passes all the remaining arguments to it verbatim. Then su cassandra -s /bin/bash runs /bin/bash and passes all the remaining arguments to it verbatim. These steps are irrelevant to your issue. The important thing is what the inner Bash gets.

Your first command

sudo su cassandra -s /bin/bash -c "/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password $@"

is very flawed. After sudo and su do their thing, bash runs as if user cassandra did:

# flawed
/bin/bash -c "something $@"

The immediate cause of your problem is the fact that double quoted $@ gets expanded by the outer shell before sudo (and su and the inner bash) even starts. Double-quoted $@ expands to (possibly) separate arguments. The first of them gets concatenated with the string (if any) preceding $@ in double-quotes; the last of them gets concatenated with the string (if any) following $@ in double-quotes. When you pass help and status, it's as if user cassandra run:

/bin/bash -c "something help" "status"

and only something help is the code. That's why status was ignored.

The version that uses "something '$@'" gets $@ expanded by the outer shell as well (the outer quotes matter) and results in a command like:

/bin/bash -c "something 'help" "status'"

which is even worse because something 'help interpreted as code is syntactically invalid; there is unmatched single-quote there, hence unexpected EOF.

In general allowing some outer tool to expand anything in a string that becomes code for the inner tool is safe only if you totally control the outcome of the expansion. If it's not known in advance nor sanitized (which may be hard) then you'll have code injection vulnerability. E.g. when you run this

bash -c "echo $variable"

and the $variable happens to expand to ; rm -f important_file then you will effectively run

bash -c "echo; rm -f important_file"

You cannot fix this by quoting. E.g. this

bash -c "echo '$variable'"

will miserably fail if $variable expands to '; rm -f important_file' (where single-quotes belong to the variable).

Your code was flawed not only because $@ could expand to multiple arguments which resulted in all but one being ignored. The argument that was not ignored was able to inject code.

This is a general problem, not only in a shell+shell scenario. The outer tool can e.g. be find.


Towards the solution

The right thing to do is to pass positional parameters of the outer shell as positional parameters (not code) to the inner shell. This try of yours:

sudo su cassandra -s /bin/bash -c '/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password "$@"' -- "$@"

is almost the right thing.

This is as if user cassandra run:

/bin/bash -c 'something "$@"' -- "$@"

In the subject of quoting and avoiding code injection this is very good. The code is single-quoted, so the outer shell does not expand the first $@. In the code interpreted by the inner shell $@ is double-quoted as it should be. This part is fine.

The problem is in -- "$@". Bash follows this convention where -- option denotes end of options. It's good to use it in case something that looks like an option appears from the expansion of the second $@ (performed by the outer shell). The first non-option argument following -c 'shell code' becomes $0 in the inner shell. What follows becomes $1, $2 etc. In your case these come from the expansion of the second $@, but the outer $1 becomes the inner $0, the outer $2 becomes the inner $1 etc. That's why when you passed help and status, help "disappeared".

The solution is to provide a "dummy" argument that will become the inner $0. This way the outer $1 will become the inner $1 etc. Such "dummy" argument is not really a dummy, it has its purpose. Please read What is the second sh in sh -c 'some shell code' sh?

So the snippet should be like:

/bin/bash -c 'something "$@"' -- some-name "$@"

Now you want to add sudo su … in front. I tested sudo: in general it supports --, but after what looks like a command (e.g. su in sudo su …) it neither needs nor interprets --. It's different with su itself. At first I thought su uses everything after -s to build a command. Then I thought it uses everything after -c as arguments. In any of these cases -- wouldn't be needed and I expected su to behave somewhat like sudo.

But no. In my Debian 10 this command:

su kamil A -c B -s /bin/echo C -c D E -- -c F G

works as if user kamil run:

/bin/echo -c D A C E -c F G

The option-argument to -s can be any executable. su searches for options for itself until it encounters --, no matter if beyond -s or -c. The latest -c before -- "wins". Then the tool passes its own option-argument found after -c to the executable along with a leading -c argument, followed by any arguments it didn't recognize as options or option-arguments to itself.

This means you need -- destined for su to actually pass -- to the shell. You want something like:

sudo su cassandra -s /bin/bash -c 'something "$@"' -- -- some-name "$@"

I've encountered su that in some (not all) circumstances "consumed" -- -- instead of --. It was probably a weird bug. Separating the two double-dashes with another argument (if possible) seemed to be a workaround. So this may be little better:

sudo su cassandra -s /bin/bash -c 'something "$@"' -- some-name -- "$@"

And because the inner shell is used only to run a single command, then maybe his:

sudo su cassandra -s /bin/bash -c 'exec something "$@"' -- some-name -- "$@"

But since -s accepts any executable and you can pass any argument to it (even without it supporting -c), you can get rid of /bin/bash completely:

sudo su cassandra -s something -- "$@"

The two solutions in the beginning of the answer are adaptations of the above two "templates".