I have a script that I want it to receive a number of variable arguments and pass them to the execution of another script, but I'm only being able to pass the first argument set by the first call.
My script (nodetool_improved
) looks like this:
sudo su cassandra -s /bin/bash -c "/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password $@"
If I call it like:
$ nodetool_improved status
Then /usr/local/cassandra/bin/nodetool
receives status
, and the output looks OK. But if I call my script like:
$ nodetool_improved help status
Then the output instead of showing the help for the status
option, just shows the help for all options, which is effectively the same as calling:
$ /usr/local/cassandra/bin/nodetool help
That means that help status
is not being passed to /usr/local/cassandra/bin/nodetool
. Only help
.
How can I change my nodetool_improved
script so that all arguments are passed to /usr/local/cassandra/bin/nodetool
?
Some other info
- Nodetool is a tool to manage Apache Cassandra: https://cassandra.apache.org/doc/latest/tools/nodetool/help.html
cassandra
user is nologin
Some things I've tried and which don't work:
sudo su cassandra -s /bin/bash -c "/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password -- '$@'"
Error:
status': -c: line 0: unexpected EOF while looking for matching `''
status': -c: line 1: syntax error: unexpected end of file
sudo su cassandra -s /bin/bash -c '/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password "$@"' -- "$@"
Error:
Only the status
gets picked up. The output is the same as if I called it like:
/usr/local/cassandra/bin/nodetool status
Best Answer
tl;dr
Or even
Analysis
At first let's explicitly state that in your commands
sudo su
runssu
and passes all the remaining arguments to it verbatim. Thensu 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
is very flawed. After
sudo
andsu
do their thing,bash
runs as if usercassandra
did:The immediate cause of your problem is the fact that double quoted
$@
gets expanded by the outer shell beforesudo
(andsu
and the innerbash
) 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 passhelp
andstatus
, it's as if usercassandra
run:and only
something help
is the code. That's whystatus
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:which is even worse because
something 'help
interpreted as code is syntactically invalid; there is unmatched single-quote there, henceunexpected 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
and the
$variable
happens to expand to; rm -f important_file
then you will effectively runYou cannot fix this by quoting. E.g. this
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:
is almost the right thing.
This is as if user
cassandra
run: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 passedhelp
andstatus
,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 insh -c 'some shell code' sh
?So the snippet should be like:
Now you want to add
sudo su …
in front. I testedsudo
: in general it supports--
, but after what looks like a command (e.g.su
insudo su …
) it neither needs nor interprets--
. It's different withsu
itself. At first I thoughtsu
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 expectedsu
to behave somewhat likesudo
.But no. In my Debian 10 this command:
works as if user
kamil
run: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 forsu
to actually pass--
to the shell. You want something like: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:And because the inner shell is used only to run a single command, then maybe his:
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:The two solutions in the beginning of the answer are adaptations of the above two "templates".