It's Not Easy Being Omnipotent (Perl for System Administration) Book Home Perl for System AdministrationSearch this book

1.6. It's Not Easy Being Omnipotent

Before we continue with the book, let's take a few minutes for some cautionary words. Programs written for system administration have a twist that makes them different from most other programs. On Unix and NT/2000 they are often run with elevated privileges, i.e., as root or Administrator. With this power comes responsibility. There is an extra onus on us as programmers to write secure code. We write code that can and will bypass the security restrictions placed on mere mortals. If we are not careful, less "ethical" users may use flaws in our code for nefarious purposes. Here are some of the issues you should consider when you use Perl under these circumstances.

1.6.1. Don't Do It

By all means, use Perl. But if you can, avoid having your code run in a privileged context. Most tasks do not require root or Administrator privileges. For example, your log analysis program probably does not need to run as root. Create another, less privileged user for this sort of automation. Have a small, dedicated, privileged program hand the data to that user if necessary, and then use that user to perform the analysis.

1.6.2. Drop Your Privileges as Soon as Possible

Sometimes you can't avoid running a script as root or Administrator. For instance, a mail delivery program you create may need to be able to write to a file as any user on the system. Programs like these should shed their omnipotence as soon as possible during their run.

Perl programs running under Unix and Linux can set the $< and $> variables:

# permanently drops privs
($<,$>) = (getpwnam('nobody'),getpwnam('nobody'));

This sets the real and effective user IDs to that of nobody, hopefully an underprivileged user. To be even more thorough, you may wish to use $( and $)to change the real and effective group IDs as well.

Windows NT and Windows 2000 do not have user IDs per se, but there are similar processes for dropping privileges. Windows 2000 has a feature called "RunAs" which can be used to run processes as a different user. Under both Windows NT and Windows 2000, users with the user right of Act as part of the operating system can impersonate other users. This user right can be set using the User Manager or User Manager for Domains program:

  1. Under the "Policies" menu, choose "User Rights."

  2. Select the "Show Advanced User Rights" check box.

  3. Choose "Act as part of the operating system" from the drop-down selector.

  4. Select "Add..." and choose the users or groups who should receive this right. You may need to choose "Show Users" if you will be adding this right to a specific user.

  5. If this is an interactive user, that user must log out and back in again to make use of this new user right.

You will also need to add the rights Replace a process level token and in some cases Bypass traverse checking (see the Win32::AdminMisc documentation). Once you have assigned these rights to a user, that user can run Perl scripts with LogonAsUser( ) from David Roth's Win32::AdminMisc module found at http://www.roth.net:

use Win32::AdminMisc;
die "Unable to impersonate $user\n" 
    if (!Win32::AdminMisc::LogonAsUser('',$user,$userpw);

Note: there is some danger here, because unlike the previous example, you must pass the password of the user to the LogonAsUser( ) call.

1.6.3. Be Careful When Reading Data

When reading important data like configuration files, test for unsafe conditions first. For instance, you may wish to check that the file and all of the directories that hold the file are not writeable (since that means someone could have tampered with them). There's a good recipe for testing this in Chapter 8 of the Perl Cookbook, by Tom Christiansen and Nathan Torkington (O'Reilly).

The other concern is user input. Never trust that input from a user is palatable. Even if you explicitly print Please answer Y or N:, there is nothing preventing the user from answering with 2049 random characters (either out of spite, malice, or because they stepped away from the keyboard and a two-year-old came over to the keyboard instead).

User input can be the cause of even more subtle trouble. My favorite example is the "Poison NULL Byte" exploit as reported in an article on Perl CGI problems. Be sure to see the whole article (cited in the References section at the end of this chapter). This particular exploit takes advantage of the difference between Perl's handling of a NULL (\000) byte in a string and the handling done by the C libraries on a system. To Perl, there is nothing special about this character. To the libraries, this character is used to indicate the end of a string.

In practical terms, this means that it is possible for a user to evade simple security tests. One example given in the article is that of a password-changing program whose code looks like this:

if ($user ne "root"){ <call the necessary C library routine>}

If $user is set to root\000 (i.e., root followed by a NULL byte) then the above test will succeed. When that string is passed to the underlying library, the string will be treated as just root, and someone will have just walked right past the security check. If not caught, this same exploit will allow access to random files and other resources. The easiest way to avoid being caught by this exploit is to sanitize your input with something like:

$input =~ tr /\000//d;

This is just one example of how user input can get our programs in trouble. Because user input can be so problematic, Perl has a security precaution called "taint mode." See the perlsec manpage that ships with Perl for an excellent discussion of "taintedness" and other security precautions.

1.6.4. Be Careful When Writing Data

If your program can write or append to every single file on the local filesystem, you need to take special care with the how, where, and when it writes data. On Unix systems, this is especially important because symbolic links make file switching and redirection easy. Unless your program is diligent, it may find itself writing to the wrong file or device. There are two classes of programs where this concern comes especially into play.

Programs that append data to a file fall into the first class. The steps your program should take in sequence before appending to a file are:

  1. Check the file's attributes before opening it using stat( ) and the normal file test operators. Make sure it is not a hard or soft link, that it has the appropriate permissions and ownership, etc.

  2. Open the file for appending.

  3. stat( ) the open filehandle.

  4. Compare the values from steps 1 and 3 to be sure that you have an open file handle to the file you intended.

You can see the bigbuffy program in Chapter 9, "Log Files", for sample code that uses this procedure.

Programs that use temporary files or directories are in the second class. You've often seen code like this:

open(TEMPFILE,">/tmp/temp.$$") or die "unable to write /tmp/temp.$$:$!\n";

Unfortunately, that's not sufficiently secure on a multiuser machine. The process ID ($$) sequence on most machines is easily predictable, which means the next temporary filename your script will use is equally predictable. If someone can predict that name, they may be able to get there first and that's usually bad news.

Some operating systems have library calls that will produce a temporary filename using a decent randomization algorithm. To test your operating system, you can run the following code. If the printed names look reasonably random to you, POSIX::tmpnam( ) is a safe bet. If not, you may have to roll your own random filename generation function:

use POSIX qw(tmpnam);
for (1..20){ print POSIX::tmpnam(  ),"\n"; }

Once you have a filename that cannot be guessed, you will need to open it securely:

sysopen(TEMPFILE,$tmpname,O_RDWR|O_CREAT|O_EXCL,0666);

There is a second, easier way to perform these two steps (getting and opening a temporary file). The IO::File->new_tmpfile( ) method from the IO::File module will not only pick a good name (if the system libraries support this), but it will also open the file for you for reading and writing.

Examples of POSIX::tmpnam( ) and IO::File->new_tmpfile( ) along with other information about this topic can be found in Chapter 7 of the Perl Cookbook. Tim Jenness' File::Temp module also attempts to provide secure temporary file operations.

1.6.5. Avoid Race Conditions

Whenever possible, avoid writing code that is susceptible to race condition exploits. The traditional race condition starts with the assumption that the following sequence is valid:

  1. Your program will amass some data.

  2. Your program can then act on that data.

If users can break into this sequence, let's say at step 1.5, and make some key substitutions, they may cause trouble. If they can get your program in step 2 to naively act upon different data than it found in step 1, they have effectively exploited a race condition (i.e., their program won the race to get at the data in question). Other race conditions occur if you do not handle file locking properly.

Race conditions often show up in system administration programs that scan the filesystem as a first pass and then change things in a second pass. Nefarious users may be able to make changes to the filesystem right after the scanner pass so that changes are made to the wrong file. Make sure your code does not leave gaps like this open.

1.6.6. Enjoy

It is important to remember that system administration is fun. Not all the time, and not when you have to deal with the most frustrating of problems, but there's a definite enjoyment to be found. There is a real pleasure in supporting other people and building the infrastructures that make other people's lives better. When the collection of Perl programs you've just written brings other people together for a common purpose, there is joy.

Now that you are ready, let's get to work on those wires.



Library Navigation Links

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