Aucbvax.2811 fa.unix-wizards utzoo!decvax!ucbvax!unix-wizards Tue Aug 25 21:47:01 1981 >From CSVAX.mark@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! ----------------------------------------------------------------- gopher://quux.org/ conversion by John Goerzen of http://communication.ucsd.edu/A-News/ This Usenet Oldnews Archive article may be copied and distributed freely, provided: 1. There is no money collected for the text(s) of the articles. 2. The following notice remains appended to each copy: The Usenet Oldnews Archive: Compilation Copyright (C) 1981, 1996 Bruce Jones, Henry Spencer, David Wiseman.