Practical UNIX & Internet Security

Practical UNIX & Internet SecuritySearch this book
Previous: 23.1 One Bug Can Ruin Your Whole Day...Chapter 23
Writing Secure SUID and Network Programs
Next: 23.3 Tips on Writing Network Programs
 

23.2 Tips on Avoiding Security-related Bugs

Software engineers define errors as mistakes made by humans when designing and coding software. Faults are manifestations of errors in programs that may result in failures. Failures are deviations from program specifications. In common usage, faults are called bugs.

Why do we bother to explain these formal terms? For three reasons:

  1. To remind you that although bugs (faults) may be present in the code, they aren't necessarily a problem until they trigger a failure. Testing is designed to trigger such a failure before the program becomes operational...and results in damage.

  2. Bugs don't suddenly appear in code. They are there because some person made a mistake - from ignorance, from haste, from carelessness, or for some other reason. Ultimately, unintentional flaws that allow someone to compromise your system are caused by people who made errors.

  3. Almost every piece of UNIX software has been developed without comprehensive specifications. As a result, you cannot easily tell when a program has actually failed. Indeed, what appears to be a bug to users of the program might be a feature that was intentionally planned by the program's authors.[5]

    [5] "It's not a bug, it's a feature!"

When you write a program that will run as superuser or in some other critical context, you must try to make the program as bug free as possible because a bug in a program that runs as superuser can leave your entire computer system wide open.

Of course, no program can be guaranteed perfect. A library routine can be faulty, or a stray gamma ray may flip a bit in memory to cause your program to misbehave. Nevertheless, there are a variety of techniques that you can employ when writing programs that will tend to minimize the security implications of any bugs that may be present. You can also program defensively to try to counter any problems that you can't anticipate now.

Here are some general rules to code by:

  1. Carefully design the program before you start.

    Be certain that you understand what you are trying to build. Carefully consider the environment in which it will run, the input and output behavior, files used, arguments recognized, signals caught, and other aspects of behavior. Try to list all of the errors that might occur, and how you will deal with them. Consider writing a specification document for the code. If you can't or won't do that, at least consider writing documentation including a complete manual page before you write any code. That can serve as a valuable exercise to focus your thoughts on the code and its intended behavior.

  2. Check all of your arguments.

    An astonishing number of security-related bugs arise because an attacker sends an unexpected argument or an argument with unanticipated format to a program or a function within a program. A simple way to avoid these kinds of problems is by having your program always check all of its arguments. Argument checking will not noticeably slow down most programs, but it will make them less susceptible to hostile users. As an added benefit, argument checking and error reporting will make the process of catching non-security-related bugs easier.

    When you are checking arguments in your program, pay extra attention to the following:

    • Check arguments passed to your program on the command line. Check to make sure that each command-line argument is properly formed and bounded.

    • Check arguments that you pass to UNIX system functions. Even though your program is calling the system function, you should check the arguments to be sure that they are what you expect them to be. For example, if you think that your program is opening a file in the current directory, you might want to use the index( ) function to see if the filename contains a slash character (/). If the file does contain the slash, and it shouldn't, the program should not open the file.

    • Check arguments passed in environment variables to your program, including general environment variables and such variables as the LESS argument.

    • Do bounds checking on every variable. If you only define an option as valid from 1 to 5, be sure that no one tries to set it to 0, 6, -1, 32767, or 32768. If string arguments are supposed to be 16 bytes or less, check the length before you copy them into a local buffer (and don't forget the room required for the terminating null byte). If you are supposed to have three arguments, be sure you got three.

  3. Don't use routines that fail to check buffer boundaries when manipulating strings of arbitrary length.

    In the C programming language particularly, note the following:

    Avoid

    Use Instead

    gets()

    fget()

    strcpy()

    strncpy()

    strcat()

    strncat()

    Use the following library calls with great care - they can overflow either a destination buffer or an internal, static buffer on some systems if the input is "cooked" to do so: [6] sprintf( ), fscanf( ), scanf( ), sscanf( ), vsprintf( ), realpath( ), getopt( ), getpass( ), streadd( ), strecpy( ), and strtrns( ). Check to make sure that you have the version of the syslog() library which checks the length of its arguments.

    [6] Not all of these will be available under every version of UNIX.

    There may be other routines in libraries on your system of which you should be somewhat cautious. Note carefully if a copy or transformation is performed into a string argument without benefit of a length parameter to delimit it. Also note if the documentation for a function says that the routine returns a pointer to a result in static storage. If an attacker can provide the necessary input to overflow these buffers, you may have a major problem.

  4. Check all return codes from system calls.

    The UNIX operating system has almost every single system call provide a return code. Even system calls that you think cannot fail, such as write(), chdir(), or chown(), can fail under exceptional circumstances and return appropriate return codes.

    When the calls fail, check the errno variable to determine why they failed. Have your program log the unexpected value and then cleanly terminate if the system call fails for any unexpected reason. This approach will be a great help in tracking down problems later on.

    If you think that a system call should not fail and it does, do something appropriate. If you can't think of anything appropriate to do, then have your program delete all of its temporary files and exit.

  5. Don't design your program to depend on UNIX environment variables.

    The simplest way to write a secure program is to make absolutely no assumptions about your environment and to set everything explicitly (e.g. signals, umask, current directory, environment variables). A common way of attacking programs is to make changes in the runtime environment that the programmer did not anticipate.

    Thus, you want to make certain that your program environment is in a known state. Here are some of the things you want to do:

    • If you absolutely must pass information to the program in its environment, then have your program test for the necessary environment variables and then erase the environment completely.

    • Otherwise, wipe the environment clean of all but the most essential variables. On most systems, this is the TZ variable that specifies the local time zone, and possibly some variables to indicate locale. Cleaning the environment avoids any possible interactions between it and the UNIX system libraries.

    • You might also consider constructing a new envp and passing that to exec(), rather than using even a scrubbed original envp. Doing so is safer because you explicitly create the environment rather than trying to clean it.

    • Make sure that the file descriptors that you expect to be open are open, and that the file descriptors you expect to be closed are closed.

    • Ensure that your signals are set to a sensible state.

    • Set your umask appropriately.

    • Explicitly chdir() to an appropriate directory when the program starts.

    • Set whatever limit values are necessary so that your program will not leave a core file if it fails. Consider setting your other limits on number of files and stack size to appropriate values if they might not be appropriate at program start.

  6. Have internal consistency-checking code.

    Use the assert macro if you are programming in C. If you have a variable that you know should either be a 1 or a 2, then your program should not be running if the variable is anything else.

  7. Include lots of logging.

    You are almost always better having too much logging rather than too little. Report your log information into a dedicated log file. Or, consider using the syslog facility, so that logs can be redirected to users or files, piped to programs, and/or sent to other machines. And remember to do bounds checking on arguments passed to syslog() to avoid buffer overflows.

    Here is specific information that you might wish to log:

    • The time that the program was run.

    • The UID and effective UID of the process.

    • The GID and effective GID of the process.

    • The terminal from which it was run.

    • The process number (PID).

    • Command-line arguments.

    • Invalid arguments, or failures in consistency checking.

    • The host from which the request came (in the case of network servers).

  8. Make the critical portion of your program as small and as simple as possible.

  9. Read through your code.

    Think of how you might attack it yourself. What happens if the program gets unexpected input? What happens if you are able to delay the program between two system calls?

  10. Always use full pathnames for any filename argument, for both commands and data files.

  11. Check anything supplied by the user for shell meta characters if the user-supplied input is passed on to another program, written into a file, or used as a filename. In general, checking for good characters is safer than checking for a set of "bad characters" and is not that restrictive in most situations.

  12. Examine your code and test it carefully for assumptions about the operating environments. For example:

    • If you assume that the program is always run by somebody who is not root, what happens if the program is run by root? (Many programs designed to be run as daemon or bin can cause security problems when run as root, for instance.)

    • If you assume that it will be run by root, what happens if it is not run as root?

    • If you assume that a program always runs in the /tmp or /tmp/root [7] directory, what happens if it is run somewhere else?

      [7] We use /tmp/root, with the understanding that youhave a directory /tmp/root automatically created by your start-up scripts, and that this directory has a mode of 0700. Your /tmp directory should have mode 1777, which prevents ordinary users from deleting the /tmp/root directory.

  13. Make good use of available tools.

    If you are using C and have an ANSI C compiler available, use it, and use prototypes for calls. If you don't have an ANSI C compiler, then be sure to use the -Wall option to your C compiler (if supported) or the lint program to check for common mistakes.

  14. Test your program thoroughly.

    If you have a system based on SVR4, consider using (at the least) tcov, a statement-coverage tester. Consider using commercial products, such as CodeCenter and Purify (from personal experience, we can tell you that these programs are very useful). Look into GCT, a test tool developed by Brian Marick at the University of Illinois.[8] Remember that finding a bug in testing is better than letting some anonymous system cracker find it for you!

    [8] Available for FTP from ftp.cs.uiuc.edu.

  15. Be aware of race conditions. These can be manifest as a deadlock, or as failure of two calls to execute in close sequence.

    • Deadlock conditions. Remember: more than one copy of your program may be running at the same time. Consider using file locking for any files that you modify. Provide a way to recover the locks in the event that the program crashes while a lock is held. Avoid deadlocks or "deadly embraces," which can occur when one program attempts to lock file A then file B, while another program already holds a lock for file B and then attempts to lock file A.

    • Sequence conditions . Be aware that your program does not execute atomically. That is, the program can be interrupted between any two operations to let another program run for a while - including one that is trying to abuse yours. Thus, check your code carefully for any pair of operations that might fail if arbitrary code is executed between them.

      In particular, when you are performing a series of operations on a file, such as changing its owner, stat'ing the file, or changing its mode, first open the file and then use the fchown(), fstat(), or fchmod() system calls. Doing so will prevent the file from being replaced while your program is running (a possible race condition). Also avoid the use of the access() function to determine your ability to access a file: Using the access() function followed by an open() is a race condition, and almost always a bug.

  16. Don't have your program dump core except during your testing.

    Core files can fill up a filesystem. Core files can contain confidential information. In some cases, an attacker can actually use the fact that a program dumps core to break into a system. Instead of dumping core, have your program log the appropriate problem and exit. Use the setrlimit() function to limit the size of the core file to 0.

  17. Do not provide shell escapes (with job control, they are no longer needed).

  18. Never use system() or popen() calls.

    Both invoke the shell, and can have unexpected results when they are passed arguments with funny characters, or in cases in which environment variables have peculiar definitions.

  19. If you are expecting to create a new file with the open call, then use the O_EXCL | O_CREAT flags to cause the routine to fail if the file exists.

    If you expect the file to be there, be sure to omit the O_CREAT flag so that the routine will fail if the file is not there.[9]

    [9] Note that on some systems, if the pathname in the open call refers to a symbolic link that names a file that does not exist, the call may not behave as you expect. This scenario should be tested on your system so you know what to expect.

  20. If you think that a file should be a file, use lstat() to make sure that it is not a link.

    However, remember that what you check may change before you can get around to opening it if it is in a public directory. (See item )

  21. If you need to create a temporary file, consider using the tmpfile( ) or mktemp( ) function.

    This step will create a temporary file, open the file, delete the file, and return a file handle. The open file can be passed to a subprocess created with fork( ) and exec( ), but the contents of the file cannot be read by any other program on the system. The space associated with the file will automatically be returned to the operating system when your program exits. If possible, create the temporary file in a closed directory, such as /tmp/root/.

    NOTE: The mktemp() library call is not safe to use in a program that is running with extra privilege. The code as provided on most versions of UNIX has a race condition between a file test and a file open. This condition is a well-known problem, and relatively easy to exploit. Avoid the standard mktemp() call.

  22. Do not create files in world-writable directories.

  23. Have your code reviewed by another competent programmer (or two, or more).

    After they have reviewed it, "walk through" the code with them and explain what each part does. We have found that such reviews are a surefire way to discover logic errors. Trying to explain why something is done a certain way often results in an exclamation of "Wait a moment ...why did I do that?"

  24. If you need to use a shell as part of your program, don't use the C shell.

    Many versions have known flaws that can be exploited, and nearly every version performs an implicit eval $TERM on start-up, enabling all sorts of attacks. Furthermore, the C shell makes it difficult to do things that you may want to do, such as capture error output to another file or pipe.

    We recommend the use of ksh93 (used for most of the shell scripts in this book). It is well designed, fast, powerful, and well documented (see Appendix D).

Remember, many security bugs are actually programming bugs, which is good news for programmers. When you make your program more secure, you'll simultaneously be making it more reliable.


Previous: 23.1 One Bug Can Ruin Your Whole Day...Practical UNIX & Internet SecurityNext: 23.3 Tips on Writing Network Programs
23.1 One Bug Can Ruin Your Whole Day...Book Index23.3 Tips on Writing Network Programs