James B. Byrne James B. Byrne - 9 months ago 50
Perl Question

How does one provide IPC::Run with an argument set by user input that contains embedded whitespace

centos-6.8 perl, v5.10.1 (*) built for x86_64-linux-thread-multi

This question descends from this one Where is the shell command called which invokes OpenSSL commands?. Briefly I am hacking a very old Perl script used to maintain an internal private PKI so that the default signature hashes and key sizes meet current browser requirements.

I have these snippets of code:

. . .
$args->{keypass} = $self->getPassword("Private key password",1)
unless $args->{keypass};
$self->warn("# Password argument: $args->{keypass}\n") if $ENV{CSPDEBUG};
my $cmd = "-out $args->{keyfile} $args->{keysize}";
$cmd = "-des3 -passout pass:$args->{keypass} ".$cmd if defined($args->{keypass});
. . .
$self->{openssl}->cmd('req',"-x509 $common_args -new -out $cacert",$args);
. . .
use IPC::Run qw( start pump finish timeout new_appender new_chunker);
. . .
sub cmd
my $self = shift;
my $cmd = shift;
my $cmdline = shift;
my $args = shift;
my $conf;
my $cfgcmd;
. . .
$self->{_handle}->pump while length ${$self->{_in}};
. . .

If the password argument value that the user provides contains no white space then this code performs as desired. If it does contain embedded white space then the code fails silently. If the argument passed to
is concatenated with starting and ending single-quotes then the code likewise fails silently. In both cases of failure the script nonetheless reports success.


What change is necessary to make this code work whether the user input contains spaces or not?

Answer Source

To answer your literal question, let me quote the IPC::Run manual:

"run(), start(), and harness() can all take a harness specification as input. A harness specification is either a single string to be passed to the systems' shell […] or a list of commands, io operations, and/or timers/timeouts to execute."

To prevent the command arguments being parsed by the shell (which is what's causing things to break when the arguments contain spaces), you should not pass them as a single string, but as a reference to an array that contains each individual argument as a single string, something like this:

my @cmd = ("-out", $args->{keyfile}, $args->{keysize});
unshift @cmd, ("-des3", "-passout", "pass:$args->{keypass}") if defined $args->{keypass};
# ...
my $h = start ["openssl", "genrsa", @cmd], \$in, \$out;  # or something equivalent

(The code you've posted seems to be using IPC::Run via some custom interface layer; since you haven't shown us exactly what that layer looks like, I've replace it with a simple call to IPC::Run::start.)

In any case, note that passing passwords on the command line is generally considered insecure: if any untrusted users can run code on the same server (even under an unprivileged account), they can see the password simply by running ps ax. The openssl manual notes this, and warns that pass:password "should only be used where security is not important."

A safer alternative would be to send the password over a separate file descriptor. Conveniently, IPC::Run makes this pretty easy:

my @cmd = ("-out", $args->{keyfile}, $args->{keysize});
unshift @cmd, ("-des3", "-passout", "fd:3") if defined $args->{keypass};
# ...
my $h = start ["openssl", "genrsa", @cmd], '<', \$in, '>', \$out, '3<', \$args->{keypass};

Here, the password is passed over the file descriptor number 3; if you need to pass in multiple passwords, you can use file descriptors 4, 5, etc. for those. (Descriptors 0, 1 and 2 are stdin, stdout and stderr.)

Disclaimer: This is, obviously, all untested code. I'm not an expert on IPC::Run, so I may have made some silly command syntax errors or other mistakes. Please test thoroughly before using!