Sensitive data in process substitution

encryptionpermissionsprocess-substitutionSecurity

Let's say I do:

#!/bin/bash
#content=$(cat -)
content="foo"
pass=$1
echo $content | ccrypt -f -k <(echo -n $pass)  

Can the permission set on the process substition /dev/fd/* files be trusted to keep the passphrase safe for the duration of ccrypt?

EDIT: Removed stupid offending line from script revealing pass anyway

Best Answer

Let's break it down:

content=$(cat -)

That's the same as content=$(cat). That's using command substitution which in bash uses a pipe. At one end of the pipe, cat is writing what it reads from its stdin. And bash at the other end is reading that to store into $content.

However, before doing that, it strips trailing newline characters and also would choke on NUL characters. To be able to store arbitrary data in a variable, you can't use bash; you'd need to use zsh instead and do something like:

content=$(cat; echo .); content=${content%.}

to work around the newline stripping issue.

Note that the data to be stored in $content is fed through the stdin of your script. That would be available (on Linux) as /proc/pid-of-your-script/fd/0 which is the same as /proc/pid-of-cat/fd/0. And since cat copies it to a pipe to bash, it's also in /proc/pid-of-cat/fd/1 and /proc/pid-of-script/fd/fd-to-the-other-end-of-the-pipe.

In any case here, you're using the content of $content only once so it doesn't make sense to do that intermediate step.


pass=$1

Here, you're saying that the very secret key is taken from the first argument of the script.

You cannot pass secret data as a command line arguments. Command line arguments are not secret. They show in the output of ps -efwww. On Linux, /proc/pid/cmdline which contains them is world-readable (by default; on Linux, access to /proc/pid can be administratively restricted to processes with the same euid though that's rarely done as that affects the behaviour or ps among other things).

On some systems, they may even be logged via some process accounting/auditing mechanism.


echo $content is wrong for several reasons:

  • echo cannot be used for arbitrary data. It doesn't work properly with arguments like -n, -neneneene... and depending on the environment things that contain backslashes.
  • An unquoted $content means invoking the split+glob operator which you do not want and would cause problems if $content contains characters of $IFS or wildcards.
  • it adds an extra newline character.

You'd want:

printf %s "$content"

And make sure trailing newlines have not been stripped earlier.

As that printf is part of a pipeline, it will be run in a child process whose stdout will be a pipe. On Linux, /proc/pid-of-that-child-process/fd/1 will give access to that content. So will /proc/pid-of-ccript/0.


In

ccrypt -f -k <(echo -n $pass)

There's again the problem of echo and the unquoted $pass.

echo (again running in a child process) will write the password to a pipe. The other end of the pipe will be made available on fd n to ccrypt and <(...) will expand to something like /dev/fd/n or /proc/self/fd/n. ccrypt will open that file (so on a new fd), which again (on Linux) will be available as /proc/pid-of-ccrypt/fd/that-fd in addition to /proc/pid-of-ccrypt/fd/n and /proc/pid-of-echo/fd/1


Now, the major problem in your code is not process substitution or any of those other pipes, but the fact that the password is given as a command line argument of a command (here, your script).

Process substitution involves a normal pipe, just as with $(...) command substitution and |. /dev/fd/x on most systems except Linux is only meaningful to the corresponding process, so cannot leak to other processes. But other processes running as the same euid (or as root) could read the memory of those processes anyway (like debuggers do) and recover that password (or probably get it from the same source anyway).

On Linux, /dev/fd is a symlink to /proc/self/fd and /proc/self a dynamic symlink to /proc/the-pid. /proc/pid/fd is by default readable by processes with the same euid (though there can be further restrictions added, the same ones that restrict who may attach a debugger to a process).

For fds that point to pipes, /proc/pid/fd/that-fd acts like a named pipe. So another process (again running as the same euid or root) could steal the content of the pipe. But in any case, if they can do that, they can also directly read the content of the processes memory so there's no point trying to guard against that.


Instead of passing the password on a command line, you could pass it via an environment variable. The environment is a lot more private than the argument list. On Linux, /proc/pid/environ is only readable by processes with the same euid (or root).

So your script could just be:

#! /bin/sh -
exec ccrypt -f -E PASSWORD

And call it as

PASSWORD=secret-phrase the-script < data-to-encrypt 
Related Question