Over the years, we have developed a list of general principles by which
to code. What follows is an excerpt from that list, edited for its particular
relevance to CGI and API programs:
-
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. List all of the errors that might occur, and how your program
will deal with them. Write a code specification in English (or your native
language) before writing the code in the computer language of your choice.
-
Check all values provided by the user.
An astonishing number of security-related bugs arise because an
attacker sends an unexpected value or an unanticipated format to a program
or a function within a program. A simple way to avoid these types of problems
is by having your CGI programs always check all of their arguments. Argument
checking will not noticeably slow your CGI scripts, 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 nonsecurity-related
bugs easier.
When you are checking arguments in your program, pay extra attention
to the following:
-
Filter your arguments, selecting the characters that are appropriate for
each application.
-
Check the length of every argument.
-
If you use a selection list, make certain that the value provided by the
user was one of the legal values.
-
Check arguments that you pass to operating 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 in C or Perl to
see if the filename contains a slash character (/). If the file contains
a slash, and it shouldn't, the program should not open the file.
-
Check all return codes from system calls.
The POSIX programming specification (which is followed by both C
and Perl) requires that every 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 a call fails, check the errno variable to determine why it
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 both programming bugs and security 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.
-
Have internal consistency-checking code.
If you think that a variable inside your program can only have the
values 1, 2, or 3, check it to ensure that it does, and generate an error
condition if it does not. (You can do this easily using the assert macro
if you are programming in C.)
-
Include lots of logging.
You are almost always better off having too much logging rather
than too little. Rather than simply writing the results to standard error,
and relying on your web server's log file, report your log information
to a dedicated log file. It will make it easier for you to find the problems.
Alternatively, consider using the syslog facility, so that logs can be
redirected to users or files, piped to programs, and/or sent to other machines.
(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 process number (PID)
-
Values provided to the program
-
Invalid arguments, or failures in consistency checking
-
The host from which the request came; log both hostname and IP address
-
Make the critical portion of your program as small and as simple as
possible.
-
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?
-
Always use full pathnames for any filename argument, for both commands
and data files.
-
Rather than depending on the current directory, set it yourself.
-
Test your program thoroughly.
-
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. Use 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 and 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
atomatically. 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.
-
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.
-
Do not create files in world-writable directories.
If your CGI script needs to run as the nobody user, then have the
directory in which it needs to create files owned by the nobody user. (This
also applies to the /tmp directory.)
-
Don't place undue reliance on the source IP address in the packets of
connections you receive.
Such items may be forged or altered.
-
Include some form of load shedding or load limiting in your server to
handle cases of excessive load.
What happens if someone makes a concerted effort to direct a denial-of-service
attack against your server? One technique is to have your web server stop
processing incoming requests if the load goes over some predefined value.
-
Put reasonable time-outs on the real time used by your CGI script while
it is running.
Your CGI script may become blocked for any number of reasons: a
read request from a remote server may hang. The user's web browser may
not accept information that you send to it. An easy technique to solve
both of these problems is to put hard limits on the amount of real time
that your CGI script can use. Once it uses more than its allotted amount
of real time, it should clean up and exit. Most modern systems support
some call to set such a limit.
-
Put reasonable limits on the CPU time used by your CGI script while
it is running.
A bug in your CGI script may put it in an infinite loop. To protect
your users and your server against this possibility, you should place a
hard limit on the total amount of CPU time that the CGI script can consume.
-
Do not require the user to send a reusable password in plaintext over
the network connection to authenticate himself or herself.
If you use usernames and passwords, use a cryptographically enabled
web server so that the password is not sent in plaintext. Alternatively,
use client-side certificates to provide authentication.
-
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?"
-
"Whenever possible, steal code."
Don't write your own CGI library when you can use one that's already
been debugged. But beware of stealing code that contains Trojan horses.
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.
If you're writing scripts in Perl, you'll want to read