16.4 Tips on Writing SUID/SGID Programs
If you
are writing programs that are SUID or SGID, you must take added
precautions in your programming. An overwhelming number of Unix
security problems have been caused by SUID/SGID programs. Consider
the rules described in this section in addition to those in previous
sections.
"Don't do it. Most of the time,
it's not necessary."
Avoid writing SUID shell scripts.
If you are using SUID to access a special set of files,
don't. Instead, create a special group for your
files and make the program SGID to that group.
If you must use SUID, create a special user for the purpose.
If your program needs to perform some functions as superuser, but
generally does not require SUID permissions, consider putting the
SUID part in a different program, and constructing a carefully
controlled and monitored interface between the two.
If you need SUID or SGID permissions, use them for their intended
purpose as early in the program as possible, and then revoke them by
returning the effective, and real, UIDs and GIDs to those of the
process that invoked the program.
If you have a program that absolutely must run as SUID, try to avoid
equipping the program with a general-purpose interface that allows
users to specify much in the way of commands or options.
Erase the execution environment, if at all possible, and start fresh.
Many security problems have been caused because there was a
significant difference between the environment in which the program
was run by an attacker and the environment in which the program was
developed.
If your program must spawn processes, use only the
execve( ),
execv( ), or execl( )
calls, and use them with great care. Avoid the execlp(
) and execvp( ) calls because they
use the PATH environment variable to find an executable, and you
might not run what you think you are running. Avoid
system( ) and
popen( ) at all costs.
If you must provide a shell escape, be sure to
setgid(getgid( )) and
setuid(getuid( )) before executing the
user's command—and use them in the correct
order! You must reset the group ID before you
reset the user ID, or the call will fail.
In general, use the setuid( ) and
setgid( ) functions and their friends to bracket
the sections of your code that require superuser privileges. For
example: /* setuid program is effectively superuser so it can open the master file */
fd = open("/etc/masterfile",O_RDONLY);
assert(seteuid(getuid( )) == 0);
/* Give up superuser now, but we can get it back.*/
assert(geteuid() == getuid( ));/* Insure that the euid is what we expect. */
if(fd<0) error_open( ); /* Handle errors. */ Not all versions of Unix allow you to switch UIDs in this way;
moreover, the semantics of the various versions of setuid(
), seteuid( ), and
setreuid( ) have been shown to vary between Unix
flavors, and even be misimplemented. It's also
crucial both to check their return values and to separately test to
ensure that the UIDs are as you expect them. Read Chen, Wagner, and
Dean's paper "Setuid
Demystified" (http://www.cs.berkeley.edu/~daw/papers/setuid-usenix02.pdf)
before you even think about writing code that tries to save and
restore privileges.
If you must use pipes or subshells, be especially careful with the
environment variables PATH and
IFS. One approach is to erase these variables and set them to safe
values. For example: putenv("PATH=/bin:/usr/bin:/usr/ucb");
putenv("IFS= \t\n"); Then, examine the environment to be certain that there is only
one instance of the variable: the one you set.
An attacker can run your code from another program that creates
multiple instances of an environment variable. Without an explicit
check, you may find the first instance, but not the others; such a
situation could result in problems later on. In particular, step
through the elements of the environment yourself rather than
depending on the library getenv( ) function.
Another approach, simpler but more drastic, is to create an empty
environment and fill it with only those variables that you know are
OK. This environment can then be passed to execve(
):
char *env[MAX_ENV];
int mysetenv(const char *name, const char *value) {
static char count = 0;
char buff[255];
if (count == MAX_ENV) return 0;
if (!name || !value) return 0;
if (snprintf(buff, sizeof(buff), "%s=%s", name, value) < 0) return 0;
if (env[count] = strdup(buff)) {
count++;
return 1;
}
return 0;
}
...And then in the program...
if (mysetenv("PATH", "/bin:/usr/bin") &&
mysetenv("SHELL", "/bin/sh") &&
mysetenv("TERM", "vt100") &&
mysetenv("USER", getenv("USER")) &&
mysetenv("LOGNAME", getenv("LOGNAME")) &&
mysetenv("HOME", getenv("HOME"))) {
execve(myprogram,NULL,env);
perror(myprogram);
} else {
perror("Unable to establish safe environment");
}
Use the full pathname for all files that you open. Do not make any
assumptions about the current directory. (You can enforce this
requirement by doing a chdir("/tmp/root/") as
one of the first steps in your program, but be sure to check the
return code!)
Consider statically
linking your program. If a user can substitute a different module in
a dynamic library, even carefully coded programs are vulnerable. (We
have some serious misgivings about the trend in commercial systems
towards completely shared, dynamic libraries. (See our comments in Section 23.6.2 in Chapter 23.)
Consider using perl
-T or taintperl
for your SUID programs and scripts. Perl's tainting
features often make Perl more suited than C to SUID programming. For
example, taintperl insists that you set the PATH
environment variable to a known "safe
value" before calling system(
). The program also requires that you
"untaint" any variable that is
input from the user before using it (or any variable dependent on
that variable) as an argument for opening a file. However, note that you can still get yourself in a great deal of
trouble with taintperl if you circumvent its
checks or if you are careless in writing code. Also note that using
taintperl introduces dependence on another large
body of code working correctly: we suggest you skip using
taintperl if you believe that you can code at
least as well as Larry Wall.
|