[sf-lug] shell quoting

Michael Paoli Michael.Paoli at cal.berkeley.edu
Thu Nov 9 22:16:40 PST 2017

> From: "Alex Kleider" <akleider at sonic.net>
> Subject: Re: [sf-lug] shell quoting
> Date: Thu, 09 Nov 2017 10:35:26 -0800

> On 2017-11-08 19:24, Michael Paoli wrote:
>> Various ways, but, e.g.:
>> shell variable / named parameter is set in the outer/surrounding
>> shell, then, minimally, something like this (but possibly a bit
>> hazardous):
>> sudo sh -c 'echo "'\''$ap_ip'\''  library library.lan rachel
>> rachel.lan" >> /etc/hosts'
>> But that could be potentially rather hazardous, depending upon what
>> ap_ip is set to.
>> Safer would be something more like:
>> sudo sh -c 'echo "'\''"$ap_ip"'\''  library library.lan rachel
>> rachel.lan" >> /etc/hosts'
> I just tried the above- the line added to /etc/hosts was:
> ''  library library.lan rachel rachel.lan
> The first version above does the same.

8-O  My bad.  I flew through that one too quickly.

Should've been more like ... (and here, I change the output file also,
just for demonstration purposes, and also tighten up the bulk of the
text to X, and use a slightly shorter IP and tighten up some whitespace
that's not actually used in the output):

For minimal example:
$ ap_ip=; sudo sh -c 'echo "'$ap_ip'  X" >> /dev/tty'  X
But that first example is very minimal and hazardous at best.
Here's very slightly improved:
$ ap_ip=; sudo sh -c 'echo "'"$ap_ip"'  X" >> /dev/tty'  X
But that's still quite hazardous.

Here's somewhat safer ... at least for the command itself,
but not validating the data it will write (to /dev/tty ... or
$ ap_ip=; sudo sh -c 'echo '\'"$ap_ip"'  X'\'' >> /dev/tty'  X

And ... why?  Program defensively.  :-)

Let's say ap_ip was set to something else, and go over our examples:
$ ap_ip='`id`'; sudo sh -c 'echo "'$ap_ip'  X" >> /dev/tty'
uid=0(root) gid=0(root) groups=0(root),7(lp),126(lpadmin)  X
Note that the relatively arbitrary (but innocuous in our example)
command we "snuck" in in that variable / named parameter, was executed
as superuser (root - UID 0)!

Let's look at our second example:
$ ap_ip=\'';id;: '\'; sudo sh -c 'echo '\'"$ap_ip"'  X'\'' >> /dev/tty'
uid=0(root) gid=0(root) groups=0(root),7(lp),126(lpadmin)
Note that the relatively arbitrary (but innocuous in our example)
command we "snuck" in in that variable / named parameter, was executed
as superuser (root - UID 0)!

Now let's look at our third example:
$ ap_ip='!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~'
$ sudo sh -c 'echo '\'"$ap_ip"'  X'\'' >> /dev/tty'
!"#$%&()*+,-./:;<=>?@[\]^_`{|}~  X
So, that's pretty good, ... but still not 100%, as a single quote
within will still cause issues:
$ ap_ip=\'';id;: '\'; sudo sh -c 'echo '\'"$ap_ip"'  X'\'' >> /dev/tty'
uid=0(root) gid=0(root) groups=0(root),7(lp),126(lpadmin)

So, let's take another look at what we're doing:
sudo sh -c 'echo '\'"$ap_ip"'  X'\'' >> /dev/tty'
So, we've got shell interpolation,
which passes to sudo which uses shell and does shell interpolation
again.  So ... can we get the data of that variable/parameter literally
all the way down there?  To best do that, we should have the
interpolation done exactly once, and then used literally.
We need sudo to write the privileged file (well, at least in /etc/hosts
case).  Don't need it for anything else.  If sudo is passwordless,
as it is in our example case, we can also feed sudo data from stdin.
Even if we're doing sudo with password, if there's attached tty, sudo
would generally read authentication data (password) from that, rather
than stdin.  So ... taking those together ...
$ ap_ip='!"#$%&'\''()*+,-./:;<=>?@[\]^_`{|}~'
$ echo "$ap_ip  X" | sudo sh -c 'cat >> /dev/tty'
!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~  X
So, now we can pass through any character unchanged ...
well, except we may have to be concerned about how echo
behaves, e.g. does $ap_ip start with dash (-) or contain backslash (\) ?
The particular behavior may depend upon one's shell and echo.
There's also a POSIXy way in non-ancient shells to do something
effectively quite similar to echo, but with no further interpretation
upon string.
I'll leave that bit as exercise.  :-)
I'll also leave as exercise, the question/matter of input validation.
E.g. what if ap_ip contains newlines?  8-O
Note also we didn't use export or environment, just shell interpolation
and suitably passing along our data.
And ... internationalization and character sets ... gonna leave that one
as an exercise too.  :-)

>> Complete explanations left as an exercise.  :-)
>> Hints:
>> ' - shields from shell (quotes) everything, until matching '
>> " - quotes, but subject to variable/parameter/command substitution (and
>>    perhaps a wee bit more, depending upon what shell)
>> \ - when not itself otherwise quoted, quotes just the immediately following
>>    character
>> '\'' within a pair of '' ends up as a literal ' - take that one character
>>     at a time parsing it through slowly to understand, e.g.:
>>     ''\''' - take it character by character to see what happens.
>> with variable/parameter substitution, if not otherwise quoted, "words"
>> are split at whitespace - sometimes that's exactly what one wants/needs,
>> most (but not all) of the time that's not something one wants.
>> And remember, "$@" is your friend (usually what one wants, among
>> $*, $@, and "$@", and for completeness, "$*").
>> Remember to take things layer-by-layer, figuring out what things "look"
>> like to the shell, and what it does with them, each time shell parses
>> such.  Use of +x and/or -v can also be useful/informative.
>>> From: "Alex Kleider" <akleider at sonic.net>
>>> Subject: [sf-lug] sudo
>>> Date: Wed, 08 Nov 2017 18:30:28 -0800
>>> I am working with a headless Raspberry Pi and would like to be able
>>> to add a line to a file owned by root but would like to do this from
>>> within a bash script that itself is not run with root privileges.[1]
>>> Here is what I have come up with so far:
>>> #!/bin/bash
>>> # File: t.sh
>>> export ap_ip=
>>> sudo sh -c 'echo "$ap_ip  library library.lan rachel rachel.lan"  
>>> >>  /etc/hosts'
>>> This seems to work except for the fact that the variable ap_ip
>>> does not get inserted, only the other part of the line.
>>> I think it's because the quoting has not allowed the variable to
>>> be passed.
>>> Can anyone advise how this should be done?
>>> Thanks in advance.
>>> [1] It so happens that the Raspberry Pi does not ask for a password
>>> when an sudo command is issued (not sure how that is- not seen it
>>> on any other Linux system.)
>>> -- Alex Kleider
>>> (sent from my current gizmo)

More information about the sf-lug mailing list