2011-06-30
Some ways to test if a program securely runs other programs
Suppose that you have a program that can run other programs, and you want to find out if it securely runs the other programs. In an ideal world, the program's documentation would tell you, and you could trust it. Sadly we do not live in an ideal world.
First you need a test environment where you can control what external program your program runs and force it to actually run the program. In many cases (such as testing daemons, web servers, MTAs, and so on), the easiest test environment is a virtual machine. Next you need a program that logs its arguments in some appropriate place.
Let's say that we're testing a daemon with a configuration option
called 'av_scanner
' and that our program to report arguments
is called argreporter
.
Our first test is whether straight shell metacharacters have any effect:
av_scanner = /opt/argreporter 'test' >/tmp/canary
After you get the daemon to run your configured 'AV scanner', check
argreporter's logs; if it is run securely, it should have seen exactly
these two arguments. If it was run through the shell, it will have seen
one argument that won't have quotes (and /tmp/canary
will exist).
Variants of this are possible; for example, some programs will helpfully
run the av_scanner
command line through the shell if they see shell
metacharacters.
If your program fails this check, you can stop now. Otherwise, though,
we still need to test the substitutions that your program does. For
this, we need to find some substitution that introduces a space; the
best one is a variable substitution, because those are usually the
simplest. Suppose that we have a $recipients
variable that has a
space separated list of destinations; then:
av_scanner = /opt/argreporter $recipients
The logs should show that argreporter was called with one argument and that the argument had spaces in it. If it was called with several arguments and each argument was a single destination, your program makes substitutions before breaking the command line up into arguments and you've just seen why this is somewhere between annoying and dangerous.
(Another important test is what happens with empty or blank
substitutions. You want these to result in an argument of ''
instead of the argument just disappearing.)
Please have symmetric option negotiations in your protocols
Suppose that you are designing a protocol where the two ends (let us call them the initiator and the target) must agree on a joint set of options, actions, or whatever that they both support, will use, or whatever. If you are doing this, I have a small request: please make the option negotiation process symmetric, where the initiator and the target do the same operations (with slightly different data).
In a symmetric option negotiation process, the initiator says something like 'I support the following things' and the target replies with 'I support the following subset of the things you support'. One sign of a symmetric exchange is that if the initiator and target are equally capable, the set of options in the messages back and forth is exactly the same; the initiator says 'I support everything', and the target says 'then lets do everything'. In an asymmetric option negotiation process, well, things don't work that neatly.
Since this all sounds very abstract, let's use a concrete example; the milter protocol. In the milter protocol the MTA and the milter exchange two sets of options. The MTA opens with a bitfield of the message modification actions it supports and a bitfield of the protocol steps it is willing to skip. The milter replies with a bitfield of the message modification actions it may use and a bitfield of the protocol steps it wants the milter to skip. The negotiation of actions is a symmetric negotiation, but the negotiation of protocol steps is an asymmetric one. We can see the asymmetry in what the MTA and the milter say; the MTA will normally say 'I could skip all of the steps I support', and the milter will normally say 'I don't want you to skip anything (except maybe the steps that I don't understand)'.
The fundamental operation in a symmetric option negotiation is binary and-ing supported options together, you can do it repeatedly, and each end can use the same code. For example, you might and together what your program wants to support, what your protocol codec library can support, and what the other side is willing to support (if there is an other end). Asymmetric option negotiation has no neatly commutable operation on the target; in the milter protocol, for example, a milter support library wants to form the 'protocol steps I want you to skip' reply with:
mta_proto & (~known_proto_steps | client_skip_list)
(We want to tell the MTA to skip anything outside the library's known protocol steps plus anything that the client code wants the MTA to skip.)
A symmetric version of this would negotiate what protocol steps the MTA should perform, and would be:
mta_proto & known_proto_steps & client_do_list
If you default mta_proto
to known_proto_steps
, this code is also
how a library would form the MTA's initial supported protocol bitfield;
if you also default client_do_list
to known_proto_steps
, you
have covered the common case for both MTA and milter (where you wind
up doing all steps in the protocol).
As you can see, symmetric option negotiation is simpler, more straightforward, easier to get right, and easier to understand and convince yourself that the code is correct. I believe that these differences matter.