## 10.4. System Security Features

Unix security is a problem of legendary notoriety. Just about every aspect of a Unix system has some security issue associated with it, and it's usually the system administrator's job to worry about this issue.

> NOTE: This is not a textbook on Unix system security. Be aware that this section merely touches the tip of the iceberg and that there are myriad other aspects to Unix system security besides how the shell is set up. See the end of the chapter for one book that we recommend.

We first present a list of "tips" for writing shell scripts that have a better chance of avoiding security problems. Next we cover the restricted shell, which attempts to put a straitjacket around the user's environment. Then we present the idea of a "trojan horse," and why such things should be avoided. Finally we discuss the Korn shell's privileged mode, which is used with shell scripts that run as if the user were `root`.

### 10.4.1. Tips for Secure Shell Scripts

Here are some tips for writing more secure shell scripts, courtesy of Professor Eugene (Gene) Spafford, the director of Purdue University's Center for Education and Research in Information Assurance and Security:[142]

> [142] See http://www.cerias.purdue.edu.

Don't put dot in PATH
  This issue was described in Chapter 3. This opens the door wide for "trojan horses," described in the next section.
Protect bin directories
  Make sure that every directory in `$PATH` is writable only by its owner and by no one else. The same applies to all the programs in the bin directories.
Design before you code
  Spend some time thinking about what you want to do and how to do it; don't just type stuff in with a text editor and keep hacking until it seems to work. Include code to handle errors and failures gracefully.
Check all input arguments for validity
  If you expect a number, verify that you got a number. Check that the number is in the correct range. Do the same thing for other kinds of data; the shell's regular expression facilities are particularly useful for this.
Check error codes from all commands that can return errors
  Things you may not expect to fail might be mischievously forced to fail to cause the script to misbehave. For instance, it is possible to cause some commands to fail even as `root` if the argument is a NFS-mounted disk or a character-oriented device file.
Don't trust passed-in environment variables
  Check and reset them to known values if they are used by subsequent commands (e.g., `TZ`, `FPATH`, `PATH`, `IFS`, etc.). The Korn shell automatically resets `IFS` to its default upon startup, ignoring whatever was in the environment, but many other shells don't. In all cases it's an excellent idea to explicitly set `PATH` to contain just the system bin directories.
Start in a known place
  Explicitly cd to a known directory when the script starts so that any subsequent relative pathnames are to a known location. Be sure the that cd succeeds:

  >     cd app-dir || exit 1

Use full pathnames for commands
  Do this so you know which version you are getting, regardless of `$PATH`.
Use syslog(8) to keep an audit trail
  Log the date and time of invocation, username, etc.; see logger(1). If you don't have syslog, create a function to keep a log file:

  >     function logsys {
  >        print -r -- "$@"  >>  /var/adm/logsysfile
  >     }
  >     logsys "Run by user "  $(/bin/whoami)  "($USER) at "  $(/bin/date)

  (whoami(1) prints the login name of the effective user ID, a concept described later in this chapter.)
Always quote user input when using that input
  E.g., `"$1"` and `"$*"`. This prevents malicious user input from being further evaluated and executed.
Don't use eval on user input
  Beside quoting user input, don't hand it to the shell to reprocess with eval. If the user reads your script and sees that it uses eval, it's easy to subvert the script into doing almost anything.
Quote the results of wildcard expansion
  There are several nasty things you can do to a system administrator by creating files with spaces, semicolons, back-quotes, and so on in the filenames. If administrative scripts don't quote the filename arguments, the scripts can trash -- or give away -- the system.
Check user input for metacharacters
  Look for metacharacters such as `$` or ``` (old-style command substitution) if using the input in an eval or `$(`...`)`.
Test your code and read it critically
  Look for assumptions and mistakes that can be exploited. Put yourself into a nasty mood, and read your code with the intent of trying to figure out how to subvert it. Then fix whatever problems you find.
Be aware of race conditions
  If an attacker can execute arbitrary commands between any two commands in your script, will it compromise security? If so, find another way to do it.
Suspect symbolic links
  When chmoding or editing a file, check it to be sure it is a file and not a symbolic link to a critical system file. (Use `[[ -L` `file` `]]` or `[[ -h` `file` `]]` to test if file is a symbolic link.)
Have someone else review your code for mistakes
  Often a fresh pair of eyes can spot things that the original author of a program misses.
Use setgid instead of setuid, if possible
  These terms are discussed later in this chapter. In brief, by using setgid, you restrict the amount of damage that can be done to the group that is compromised.
Use a new user instead of root
  If you must use setuid to access a group of files, consider making a new, non-`root` user for that purpose, and setuid to it.
Limit setuid code as much as possible
  Make the amount of setuid code as small as you can. Move it into a separate program, and invoke that from within a larger script when necessary. However, be sure to code defensively as if the script can be invoked by anyone from anywhere else!

Chet Ramey, the maintainer of bash, offers the following prologue for use in shell scripts that need to be more secure:

>     # reset IFS, even though ksh doesn't import IFS from the environment,
>     # $ENV could set it
>     IFS=$' \t\n'

>     # make sure unalias is not a function, since it's a regular builtin
>     # unset is a special builtin, so it will be found before functions
>     unset -f unalias

>     # unset all aliases
>     # quote unalias so it's not alias-expanded
>     \unalias -a

>     # make sure command is not a function, since it's a regular builtin
>     # unset is a special builtin, so it will be found before functions
>     unset -f command

>     # get a reliable path prefix, handling case where getconf is not
>     # available (not too necessary, since getconf is a ksh93 built-in)
>     SYSPATH="$(command -p getconf PATH 2>/dev/null)"
>     if [[ -z "$SYSPATH" ]]; then
>             SYSPATH="/usr/bin:/bin"         # pick your poison
>     fi
>     PATH="$SYSPATH:$PATH"

### 10.4.2. Restricted Shell

The restricted shell is designed to put the user into an environment where his or her ability to move around and write files is severely limited. It's usually used for guest accounts. When invoked as rksh (or with the -r option), ksh acts as a restricted shell. You can make a user's login shell restricted by putting the full pathname to rksh in the user's /etc/passwd entry. The ksh executable file must have a link to it named rksh for this to work.

The specific constraints imposed by the restricted shell disallow the user from doing the following:

- Changing working directories: cd is inoperative. If you try to use it, you will get the error message `ksh: cd: restricted`.

- Redirecting output to a file: the redirectors `>`, `>|`, `<>`, and `>>` are not allowed. This includes using exec.

- Assigning a new value to the environment variables `ENV`, `FPATH`, `PATH`, or `SHELL`, or trying to change their attributes with typeset.

- Specifying any pathnames of commands with slashes (/) in them. The shell only runs commands found along `$PATH`.

- Adding new built-in commands with the builtin command. (This very advanced feature is outside the scope of this book.)

These restrictions go into effect after the user's .profile and environment files are run. This means that the restricted shell user's entire environment is set up in .profile. This lets the system administrator configure the environment as she sees fit.

To keep the user from overwriting ~/.profile, it is not enough to make the file read-only by the user. Either the home directory should not be writable by the user, or the commands in ~/.profile should cd to a different directory.

Two common ways of setting up such environments are to set up a directory of "safe" commands and have that directory be the only one in `PATH`, and to set up a command menu from which the user can't escape without exiting the shell. In any case, make sure that there is no other shell in any directory listed in `$PATH`; otherwise the user can just run that shell and avoid the restrictions listed earlier.

> WARNING: Although the ability to restrict the shell has been available (if not necessarily compiled in or documented) since the original Version 7 Bourne shell, it is rarely used. Setting up a usable yet correctly restricted environment is difficult in practice. So, caveat emptor.

### 10.4.3. Trojan Horses

The concept of a trojan horse was introduced briefly in Chapter 3. A trojan horse is something that looks harmless, or even useful, but which contains a hidden danger.

Consider the following scenario. User John Q. Programmer (login name `jprog`) is an excellent programmer and has quite a collection of personal programs in ~jprog/bin. This directory occurs first in the `PATH` variable in ~jprog/.profile. Since he is such a good programmer, management recently promoted him to system administrator.

This is a whole new field of endeavor, and John -- not knowing any better -- has unfortunately left his bin directory writable. Along comes W. M. Badguy, who creates the following shell script, named grep, in John's bin directory:

>     /bin/grep "$@"
>     case $(whoami) in                  Check effective user ID name
>     root)    nasty stuff here          Danger Will Robinson, danger!
>              rm ~/jprog/bin/grep       Hide the evidence
>              ;;
>     esac

In and of itself, this script can do no damage when `jprog` is working as himself. The problem comes when `jprog` uses the su(1) command. This command allows a regular user to "switch user" to a different user. By default, it allows a regular user to become `root` (as long as that user knows the password, of course). The problem is that normally, su uses whatever `PATH` it inherits.[143] In this case, `$PATH` includes ~jprog/bin. Now, when `jprog`, working as `root`, runs grep, he actually executes the trojan horse version in his bin. This version runs the real grep, so `jprog` gets the results he expects. But it also silently executes the nasty stuff here part, as `root`. This means that Unix will let the script do anything it wants to. Anything. And to make things worse, by removing the trojan horse when it's done, there's no longer any evidence.

> [143] Get in the habit of using `su -` `user` to switch to user as if the user were doing a real login. This prevents the importing of the existing `PATH`.

Writable bin directories open one door for trojan horses, as does having dot in `PATH`. Having writable shell scripts in any bin directory is another door. Just as you close and lock the doors of your house at night, you should make sure that you close any doors on your system!

### 10.4.4. Setuid and Privileged Mode

Many problems with Unix security hinge on a Unix file attribute called the setuid (set user ID) bit. This is like a permission bit (see the earlier discussion of umask): when an executable file has it turned on, the file runs with an effective user ID equal to the owner of the file. The effective user ID is distinct from the real user ID of the process, and Unix applies its permission tests to the process's effective user ID.

For example, suppose you've written a really nifty game program that keeps a private score file showing the top 15 players on your system. You don't want to make the score file world-writable, because anyone could just come along and edit the file to make themselves the high scorer. By making your game setuid to your user ID, the game program can update the file, which you own, but no one else can update it. (The game program can determine who ran it by looking at its real user ID and using that to determine the login name.)

The setuid facility is a nice feature for games and score files, but it becomes much more dangerous when used for `root`. Making programs setuid `root` lets administrators write programs that do certain things that require `root` privilege (e.g., configure printers) in a controlled way. To set a file's setuid bit, the superuser can type `chmod 4755` `filename`; the `4` is the setuid bit.

A similar facility exists at the group level, known (not surprisingly) as setgid (set group ID). Use `chmod 2755` `filename` to turn on setgid permissions. When you do an ls -l on a setuid or setgid file, the `x` in the permission mode is replaced with an `s`; for example, `-rws--s--x` for a file that is readable and writable by the owner, executable by everyone, and has both the setuid and setgid bits set (octal mode 6711).

Modern system administration wisdom says that creating setuid and setgid shell scripts is a very, very bad idea. This has been especially true under the C shell, because its .cshrc environment file introduces numerous opportunities for break-ins. In particular, there are multiple ways of tricking a setuid shell script into becoming an interactive shell with an effective user ID of `root`. This is about the best thing a cracker could hope for: the ability to run any command as `root`.

> NOTE: There is an important difference between a setuid shell script and a setuid shell. The latter is a copy of the shell executable, which has been made to belong to `root` and had the setuid bit applied. In the previous section on trojan horses, suppose that the nasty stuff here was this code:

> >     cp /bin/ksh ~badguy/bin/myls
> >     chown root ~badguy/bin/myls
> >     chmod 4755 ~badguy/bin/myls

> Remember, this code executes as `root`, so it will work. When `badguy` executes myls, it's a machine-code executable file, and the setuid bit is honored. Hello shell that runs as `root`. Goodbye security!

Privileged mode was designed to protect against setuid shell scripts. This is a set -o option (`set -o privileged` or `set -p`), but the shell enters it automatically whenever it executes a script whose setuid bit is set, i.e., when the effective user ID is different from the real user ID.

In privileged mode, when a setuid Korn shell script is invoked, the shell runs the file /etc/suid_profile. This file should be written so as to restrict setuid shell scripts in much the same way as the restricted shell does. At a minimum, it should make `PATH` read-only (`typeset -r PATH` or `readonly PATH`) and set it to one or more "safe" directories. Once again, this prevents any decoys from being invoked.

Since privileged mode is an option, it is possible to turn it off with the command `set +o privileged` (or `set +p`). But this doesn't help the potential system cracker: the shell automatically changes its effective user ID to be the same as the real user ID -- i.e., if you turn off privileged mode, you also turn off setuid.

In addition to privileged mode, ksh provides a special "agent" program, /etc/suid_exec, that runs setuid shell scripts (or shell scripts that are executable but not readable).

For this to work, the script should not start with `#! /bin/ksh`. When the program is invoked, ksh attempts to run the program as a regular binary executable. When the operating system fails to run the script (because it isn't binary, and because it doesn't have the name of an interpreter specified with `#!`), ksh realizes that it's a script and invokes /etc/suid_exec with the name of the script and its arguments. It also arranges to pass an authentication "token" to /etc/suid_exec indicating the real and effective user and group IDs of the script. /etc/suid_exec verifies that it is safe to run the script and then arranges to invoke ksh with the proper real and effective user and group IDs on the script.

Although the combination of privileged mode and /etc/suid_exec allows you to avoid many of the attacks on setuid scripts, actually writing scripts that can safely be run setuid is a difficult art, requiring a fair amount of knowledge and experience. It should be done very carefully.

In fact, the dangers of setuid and setgid shell scripts (at least for shells besides ksh) are so great that modern Unix systems, meaning both commercial Unix systems and freeware clones (4.4-BSD-derived and GNU/Linux), disable the setuid and setgid bits on shell scripts. Even if you apply the bits to the file, the operating system does not honor them.[144]

> [144] MacOS X seems to be a notable exception. Be extra careful if you run one or more such systems!

Although setuid shell scripts don't work on modern systems, there are occasions where privileged mode is still useful. In particular, there is a widely used third party program named sudo, which, to quote the web page, "allows a system administrator to give certain users (or groups of users) the ability to run some (or all) commands as `root` or another user while logging the commands and arguments." The home page for sudo is http://www.courtesan.com/sudo/. A system administrator could easily execute `sudo /bin/ksh -p` in order to get a known environment for performing administrative tasks.

Finally, if you would like to learn more about Unix security, we recommend Practical UNIX & Internet Security by Simson Garfinkel and Gene Spafford. It is published by O'Reilly & Associates.

Copyright © 2003 O'Reilly & Associates. All rights reserved.