No subject

utzoo!decvax!ucbvax!unix-wizards utzoo!decvax!ucbvax!unix-wizards
Tue Aug 25 21:47:01 AEST 1981


>From CSVAX.mark at Berkeley Sun Aug 23 06:14:07 1981
           On the Correctness of Set-User-ID programs
                     Tom Truscott (duke!trt)
                     James Ellis  (duke!jte)

The set-user-id (SUID) capability is a patented feature of UN*X,
and is used by many programs (including Duke's Usenet news
program), yet we know of no document which describes how to write
secure SUID programs.  This is not intended to be such a
document; rather, we want to discuss some of the pitfalls that
await designers of such programs.  This is partially motivated by
the recent interest in unix-wizards about SUID programs and shell
scripts.

SUID programs require particular attention to make sure that they
work correctly.  This is partly due to their nature: they often
control access to a database shared by many users, so a foul-up
affects many people, not just one.  It is also partly due to the
danger of subverting the powers of the SUID.

The SUID given a program should be as restricted as possible.  If
Duke news ran as "bin", and a security hole were found, the
entire system would be compromised.  Ideally news should have a
SUID used by no other processes or files.

                           SUID glitch
Most UNIX systems do not honor the SUID bit of a process when it
is invoked by root.  We consider this to be a bug.  In such
cases, creat(2)ed files will be owned by root, not the SUID, so
chown(2) should be used as necessary.

                             Locking
Most SUID programs need mutually exclusive access to a resource.
The UNIX manual suggests that creat(2) with mode 0 be used to
provide locking, but that technique fails when the effective uid
is root.  We recommend link(2)ing the file ".lock" (e.g.) to some
permanently allocated file.  The link(2) will fail if ".lock"
already exists, in which case the program can retry the link(2)
every few seconds until it succeeds.  After the resource has been
used, unlink(2)ing ".lock" frees it.  The UNIX-V7 passwd(1)
program does not lock writing on /etc/passwd, which is why that
file can be scrambled if two passwd(1)s run at the same time.

                   Recovering from Dead Locks
The locking method described above can be improved somewhat.  If
the link(2) fails, the program should deterine the cause.  If
"errno" is not EEXIST (".lock" exists) then something is wrong
with the locking system, such as the program not actually being
SUID.  If ".lock" does exist it is still possible that the
resource is no longer being used but the last user failed to free
it.  Suppose it is known that the resource will never be in use
for longer than N seconds.  Then if the st_ctime of stat(2) shows
that ".lock" was last changed more than N seconds ago it can be
assumed that the resource is going unused.  The program can then
free the resource (unlink(2) ".lock"), wait a few seconds (to
avoid a race), and then retry locking the resource.

Needless to say, this is not the last word in locking!  It should
be kept in mind that the complexity is not in locking per se but
rather in recovering from a breakdown in the protocol.  The
breakdown may be symptomatic of far worse problems which cannot
be solved by deleting some lock.

                      Insecurity in General
One way to subvert a SUID program is to take advantage of a
common programming error, such as failure to check an array
bound.  The famous UNIX-V6 login(1) bug is an example of this.
Also, the user profile program provided in Amdahl's Unix (UTS)
permits users to change their comment field to a (unchecked)
character string.  Since this information is in /etc/passwd, one
can add bogus user-ids with any desired permissions.  Obviously,
SUID programs must be "suspicious" and have complete error
checking of user-supplied arguments and input data.

A more direct danger is that the program might perform file I/O
or execution for the user (RUID) without first resetting the
effective id.  Some versions of the empire, snake, and adventure
games have this problem.  (For example, run adventure and type
"!sh".) We recommend that the program fork(2) and the child do
setgid(2) and setuid(2) to the RUID before doing any I/O or
program execution.  The child should also restore umask(2) and
environ(5) to their original values--see below.  The parent,
which retains SUID permission, can wait for the child to finish.
This could be a standard library routine.

Steve Bellovin has pointed out the danger of an RUID child
process inheriting sensitive open files.  Such files should be
closed before an exec(2), either via close(2) or the UNIX-V7
FIOCLEX parameter of ioctl(2).

The signal(2) system call causes another kind of problem.  If a
program fails to catch or ignore a user-causable signal such as
SIGINT then it might be terminated in a way that leaves files in
an unintended state.  Actually, in most versions of UNIX,
catching ANY user-causable signal allows the user to terminate
the program at will since caught signals are reset (at least
temporarily).  For example, Duke news fails to catch SIGPIPE, and
it *does* catch SIGINT (equally wrong).

                       Resource Exhaustion
SUID programs can often be subverted if a needed resource is
unavailable.  For example, UNIX-V6 su(1) provided a super-user
shell if it was unable to open /etc/passwd.  It is easy to invoke
su(1) with all available file descriptors taken.  We recommend
that programs use close(2) to ensure there are free file
descriptors.  Other resources include file storage space and
process limits.  Even the program's address space can be adjusted
through the size of the argument list and the environment.

                      Insecurity in UNIX-V7
UNIX-V7 added several features which complicate SUID programs.
First, the program should disable a pending alarm(2), which might
otherwise cause undesired program termination.  Second, it should
set umask(2) to an appropriate value.  Third, the exec[lv]p(2)
calls are convenient, but should never be executed as the SUID
without first restricting the environ variable PATH as necessary.
Fourth, any exec(2) call is potentially dangerous as it may pass
along the inherited environment.  This will be considered in
detail later.  We recommend changing environ to just
PATH=/bin:/usr/bin.  Both the old umask and environ values should
be saved so they can be restored for file I/O or execution as the
RUID.

Programs should not depend upon the distributed getlogin(3) or
ttyslot(3), which can be misled by rearranging file descriptors.
There is no reliable way to determine the controlling terminal or
the login name, yet some SUID programs try anyway.  UNIX-V7
mail(1) uses getlogin(3), making it possible to fake the origin
of a letter.  UNIX-V7 login(1) uses ttyname(3) to approximate the
controlling terminal, so:
        /bin/login ken 0> /dev/tty45
logs "ken" onto tty45 if it is writable.  The super-user might
have been on tty45 but will probably be unaware that someone else
now owns the terminal.  (A shell bug prevents 0> from working,
but a C program will prove the theory.) Duke avoids this problem
since users cannot run login(1) directly; instead, they must log
off and back on.  Thus login(1) is invoked only by /etc/getty and
need not be SUID.

Publicly writable directories such as /tmp are almost totally
insecure and should be avoided.  The UNIX-V7 mail(1) program
suffers, at least in theory, due to this.  The distributed
at(1)/atrun program suffers totally:
        cd /usr/spool/at
        umask 0
        passwd
        : Type ctrl-\, producing a 666 core file owned by root.
        cp bad.deed core
        mv core 81.229.000.57;  : works since directory is writable
The resulting file will be executed with root permissions.  To
avoid this problem /usr/spool/at must not be generally writable
and at(1) must be slightly modified so it can be (safely) run
SUID to root.

                     Insecurity in the Shell
The shell(1) is particularly vulnerable when SUID since it
derives much of its power from information that was inherited or
passed via the environment.  For example, the umask value must be
reset if secure files are to be created.  Also, since there is no
direct way for a shell script to close files or reduce the
environment, this burden is placed on every command it execs.  If
the environment is almost full and the shell adds say, PATH, then
it will be unable to exec any programs at all.

Obviously, PATH should be reset in every SUID shell script.
Careful examination must also be made of other variables used by
the shell such as HOME, MAIL, and arguments ($1, ...).  Filename
expansion and blank interpretation can open security holes.  Much
worse is the shell variable IFS (which we recommend be
eliminated).  Unless this variable is reset, it is easy to change
the innocuous
                echo "Enter Password"
into a SUID editor command!

More serious still is the shell's understanding of login
procedures.  Simply exec your favorite SUID shell script with the
name "-" and it will blithely execute commands from ".profile" in
the current directory.  Having the shell use HOME/.profile is no
help at all.  Even using the effective user's login_dir/.profile
is not much better unless that file is written very carefully.

It is important to note that the problems we have discussed can
apply to non-SUID programs as well.  Not only must SUID programs
and scripts be carefully written, but also any otherwise
unprivileged programs which they run.  A SUID program which execs
some shell script by a full path name has not avoided the PATH
pitfalls!



More information about the Comp.unix.wizards mailing list