Implementation
Ensure that the program
doesn't try to read or write data outside an allocated memory block (buffer
overflow)
This is an extremely common program flaw, so it deserves special attention.
An important example of this is writing data to a memory block without first
ensuring that the memory block is large enough to hold all the data. Another
important example is writing to addresses immediately below or immediately above
an allocated block. Writing outside the memory block is usually more dangerous
than reading, but both must absolutely be avoided. For example, in C/C++,
whenever a memory block is accessed through a pointer or array name (possibly
with an offset) or a string variable name, the developers must take extreme care
to avoid accessing data on either side of the memory block. A very common source
of this error is using the C runtime-library string-manipulation functions (strcpy,
sprintf, etc.) without making sure that the resulting string will fit into the
buffer.
Ensure that all resource
allocation errors are detected and handled
Checking for memory allocation errors is very important, but since such
errors are rare in normal conditions and probably yet more so during program
development, it may happen that the developers fail to write code that handles
them. The same holds for any errors occurring in the allocation of other
resources, such as disk space, file handles, communications sockets, windows,
and so on. Failure to check for these errors is a serious program flaw. Programs
released with such defects may work well for months but then they may fail under
unusual system-stress conditions.
Ensure that the program's
stack never overflows
Whenever a program calls a function using more than the space currently
available in the stack for storing the parameters of the call and the local
variables of the function (plus a few other things), the stack overflows.
Certain languages (or language processors) provide some assistance, throwing an
exception on any attempt to overflow the stack, but in the general case the
program itself must prevent such an event from happening. As with memory
allocation errors, even if the language processor checks for stack overflows,
this is of little help if the program doesn't handle a resulting exception. With
any programming language, it is much better to take preventive actions to avoid
stack overflows altogether. Special care must be taken in the functions that
implement recursive algorithms, by ensuring that function calls never nest
beyond some given practical limit. Note that in some cases a recursive loop can
involve other functions (not apparently recursive) due to two-way interactions
with the system (presence of callbacks). All the possible cases must be
carefully examined to identify the potential dangers and provide defensive
measures. The most appropriate stack size should also be carefully determined.
Check boundary conditions
This depends very much on the application, but as a general principle, the
developers should make sure that the program (or a given program fragment)
behaves correctly when the variables take certain special values such as 0, -1,
+1, the size of a container, the size of a container minus 1 or plus 1, etc.
Many programming errors are related to a wrong handling of such special cases.
Use tools for checking the correctness of the program's code
The ability of a human reader to discover defects when reviewing a program's
source code is often limited and tools can be very helpful in identifying
troublesome code. Such tools can perform various kinds of source code analysis,
which may or may not be based on observing the program while it is running.
Interestingly, code analyzer tools for the C and C++ languages largely focus on
identifying potential buffer overflows and on tracking the use of dynamically
allocated variables to reveal potential memory leaks and misuse of pointers.
These tools usually offer a tradeoff between complexity of the analysis and
performance. In general, they do a good job of finding defects, although they
may miss some of them and may also find "false positives" when faced
with unusual coding patterns (which is not necessarily a bad thing, as it may
indicate lack of clarity in the source code).
Limit use of privileged modes during
program execution
In many operating systems, there are operations that can be invoked only by
programs running in a privileged mode (an example of this is setting a listener
on a low-numbered port in UNIX). However, while the program must be running in a
privileged mode at the moment the special operating system function is called,
it doesn't have to do so during the rest of the execution. If a program error
(such as a buffer overflow) occurs while the program is running in a privileged
mode and an attacker manages to exploit the error, he/she ends up with much
greater control over the system. Special care is needed when the program invokes
some external code (a library function, another program, or a system service).
In such cases, you should ensure that the privilege levels at which the external
function or process is executed are not higher than is necessary. In general, it
is a good idea to limit the number of times, the extent of code, and the
duration of time in which the application uses a privileged mode, so that the
potential damage in case of attack is greatly reduced. (A historical example of
this vulnerability is in the UNIX sendmail program, which unnecessarily ran in a
privileged mode all the time; attackers have been able to exploit a buffer
overflow to gain full control over the entire system.)
When processing an input
message, check that the message can be safely decoded and that its contents are
valid
In general, the application should not assume that a message received from
the network is well-formed and the values of its fields after decoding are valid
and consistent according to the protocol. The only case in which the application
can safely make such assumptions is when it uses a toolkit (such as an ASN.1
toolkit) and the documentation of the toolkit states that any ill-formed or
invalid messages are not delivered to the application or are delivered with a
warning.
When processing an input
message, limit the resources (memory, disk space, CPU time) used for the message
The purpose is to protect the system from any abnormal behavior of the
application following the receipt of a message carrying some unexpected values.
The abnormal behavior may consist of entering an infinite program loop, or in
allocating a huge amount of memory or disk space, etc. In these cases, a defect
in the software is the cause of the abnormal behavior. The event may occur
accidentally, or be the result of an intentional attack performed by sending a
specially constructed message. Obviously the problem, as soon as it is
diagnosed, needs to be solved in another part of the application; yet the
suggested measure prevents worse consequences.
Make generous use of
assertions in your code
Assertions are not a substitute for error checks, but are very useful during
program development to discover bugs in the program. The purpose of assertions
is to verify those assumptions that must be true if the program is correct, and
to help to catch program bugs as early as possible in the program execution
flow. Whenever an assertion is not verified, the runtime library code throws an
exception, so that the developer is immediately informed of the problem and the
place where it occurred. (Examples of common assertions are: asserting that a
pointer is not NULL, asserting that a variable has a value within a given range,
asserting that the values of the member variables of an object are in a certain
relationship to one another, etc.) In most C/C++ compilers, assertions are
completely discarded by the language preprocessor during release builds, so that
they don't at all affect the size and performance of the final program.
Assertions are also useful as a form of in-line documentation of the program
code.
Use restrictive language
features extensively
Such a practice allows you to discover many potential logical defects at
compile time and reduce the number of potential errors that can occur at a later
time. Let the compiler help you. When declaring variables, use the most
restrictive data type that comprises all the values needed. Exploit the name-scoping
facilities of the programming language. Limit the lifetime and visibility of
variables as much as possible. In C++, use protected/private member variables
and methods to the largest extent possible. Also, in C++, make a clear
distinction between methods that modify the state of the object and methods that
don't and declare all of the latter as "const". Besides allowing early
discovery of many program defects, such a programming style allows you to convey
more semantic information to the human reader.
Compile with the highest
warning level
Let the compiler output all the warning messages it is able to produce. In
general, it is preferable to make small changes to the program to make the
compiler happy rather than suppress the output of warning messages. Some of the
warnings may seem unnecessary, but others are really helpful in spotting
programming errors. A well-written program should compile with no warnings at
all, although this is not always easy to achieve.
[ Table of Contents ]
|
|