4.2 Bad Practices
Listing all of the things that you
should do in implementing secure code is a good start. However,
we're shooting at an ever-moving target, so
it's only a start. It's equally
important to list the things you
shouldn't do. So, in this
section, we examine a list of flawed practices, and offer our
opinions and analyses of them. Note that, although we believe the
list to be highly practical, we can't possibly
presume it to be comprehensive.
We anticipate that some of our readers may find one or two of these
tips "too obvious" for inclusion.
Surely, some might say, no one would code up such mistakes! Rest
easy! Your authors have found each and every one of these frightening
creatures in living code. Further, we admit that—back in the
bad old unenlightened days—we committed some of the worst
errors ourselves.
Even after you take every precaution, you still have to rely to some
degree on the integrity of the software environment in which your
software runs, as Ken Thompson, one of the principal creators of
Unix, famously pointed out in his Turing Award lecture. His entire
speech is well worth reading. His arguments are irrefutable; his case
study is unforgettable. And his conclusion, properly considered, is
chilling:
"The moral is obvious. You can't
trust code that you did not totally create yourself... No amount of
source-level verification or scrutiny will protect you from using
untrusted code. In demonstrating the possibility of this kind of
attack, I picked on the C compiler. I could have picked on any
program-handling program such as an assembler, a loader, or even
hardware microcode. As the level of program gets lower, these bugs
will be harder and harder to detect. A well-installed microcode bug
will be almost impossible to detect."
When you picked up this book, perhaps you thought that we could offer
you certain security? Sadly, no one can. Our code operates in a
network environment a little like a software California: many
different entities contend and cooperate. All is calm on the surface;
once in a while, one of the subterranean faults demands our
attention, and our edifices come tumbling down.
|
- Don't write code that uses relative filenames
-
Filename references should be
"fully qualified." In most cases
this
means
that the filename should start with a
`/' or
`\' character. (Note that
"fully qualified" will vary by
operating system; on some systems, for example, a filename and
pathname is not fully qualified unless it is preceded with a device
name, such as C:\AUTOEXEC.BAT. Coding a relative
filename might make it possible, for example, to change a reference
from the file working directory passwd to
/etc/passwd. Under some circumstances,
especially in the case of a program that runs with privileges, this
could result in unauthorized disclosure or modification of
information.
- Don't refer to a file twice in the same program by its name
-
Open the file once by name, and use the file handle or other
identifier from that point on. Although the specifics of how to do
this will vary a bit by operating system and by programming language,
the proscribed method can give rise to race conditions. Particularly
when file references are involved, such conditions can create
critical security flaws. Making this type of mistake means that, if
an attacker can cause the operating systems to change the file (or
substitute a different one) in between the time of the two
references, your application might be fooled into trusting
information it shouldn't.
- Don't invoke untrusted programs from within trusted ones
-
This advice holds particularly true when your software is operating
in a privileged state, but it's still true at other
times. Although invoking another program may seem a useful shortcut,
be very careful before you do so. In almost every case,
it's a better idea to do the work yourself, rather
than delegating it to another piece of software. Why? Quite simply,
you can't be certain what that untrusted program is
going to do on your behalf. After all, you're
subjecting your code to tests and reviews for security flaws; why
would you invoke a program that hasn't gone through
at least the same level of review? And note that this issue of
invoking a program may not be immediately obvious. For example, a
document previewer in a web browser or file explorer may invoke
another application to display a document or image file. Could this
affect the security of your application?
- Avoid using setuid or similar mechanisms whenever possible
-
Many
popular operating systems have a mechanism whereby a program or
process can be invoked with the identity (and therefore permissions)
of an identity
other than the one that invoked the program. In Unix, this is
commonly accomplished with the setuid capability.
It's well understood that you should avoid setuid at
all costs—that its use is symptomatic of a flawed
design—because there are almost always other ways of
accomplishing the same thing in a safer way. Regardless, the use of
setuid in Unix software is still common. If you feel that you must
use setuid, then do so with extreme caution. In particular:
Do not setuid to an existing identity/profile that has interactive
login capabilities
Create a user profile just for your purpose. Ensure that the profile
has the least possible privileges to perform the task at hand (e.g.,
read/write a particular directory or file)
Remember our discussion of the principle of least privilege in Chapter 2? This is an example of how to apply that
principle in implementing your software.
- Don't assume that your users are not malicious
-
As we discussed earlier, always
double-check every piece of external information provided to your
software. In designing a firewall, a commonly cited philosophy is to
accept only that which is expressly allowed, and to reject everything
else. Apply that same principle to taking user input, regardless of
the medium. Until any information has been verified (by your code),
presume it to be malicious in intent. Failure to adopt this mindset
in implementing code can lead to common flaws such as buffer
overflows, file naming hacks, and so on.
- Don't dump core
-
Although
"dumping core" is largely a Unix
notion, the concept spans all modern operating systems. If your code
must fail, then it should fail gracefully. In this context,
graceful degradation (a principle we introduced in Chapter 2) means that you must implement your code with
all operating system specific traps and other methods in place to
prevent "ungraceful" failure. Be
cognizant of the exit state of your software. If necessary, ensure
that it fails to a safe state (perhaps a complete halt) or force the
user to re-login if the conditions warrant it. Other than the obvious
sloppiness of dropping core files all over a filesystem, the practice
of dumping core can simplify the process for a would-be attacker to
learn information about your system by examining the contents of the
core file and (possibly) finding sensitive data.
We're sure many of you would argue that the
sensitive data should have been better protected—and you would
be correct—but preventing core dumps is just another layer in a
sound layered security methodology. This holds particularly true for
programs that run in a privileged state but is nonetheless a good
practice for all kinds of programs.
- Don't assume success
-
Whenever you issue a system call (e.g., opening a file, reading from
a file, retrieving an environment variable), don't
blindly assume that the call was successful. Always interrogate the
exit conditions of the system call and ensure that you proceed
gracefully if the call failed. Ask why the call may have failed, and
see if the situation can be corrected or worked around. Although you
may feel that this is obvious, programmers all too frequently neglect
to check return codes, which can lead to race conditions, file
overwrites, and other common implementation flaws.
- Don't confuse "random" with "pseudo-random"
-
Random numbers are often
needed in software implementations, for a slew of different reasons.
The danger here comes from the definition and interpretation of the
word "random." To some,
it's sufficient to be statistically random. However,
a random number generator can be statistically random as well as
predictable, and predictability is the kiss of death for a
cryptographically sound random number generator. Choosing the wrong
source of randomness can have disastrous results for a crypto-system,
as we'll see illustrated later in this chapter.
- Don't invoke a shell or a command line
-
While popular in interactive programs,
shell
escapes, as
they're often called, are best avoided. Implementing
shell escapes is even worse when privileges are involved. If you
absolutely must write a shell escape, you must ensure that all types
of state information (e.g., user identification, privilege level,
execution path, data path) are returned to their original state
before the escape, and that they are restored upon return.
The rationale for avoiding shell escapes is similar to the rationale
for avoiding running untrusted programs from within trusted
ones—you simply don't know what the user will
do in the shell session, and that can result in compromising your
software and its environment. This advice is all the more important
when running in a privileged state but is advisable at other times as
well.
- Don't authenticate on untrusted criteria
-
Programmers often make flawed assumptions about the identity of a
user or process, based on things that were never intended to serve
that purpose, such as IP numbers, MAC addresses, or email addresses.
Entire volumes can be (and have been) written regarding sound
authentication practices. Read them, learn from them, and avoid the
mistakes of others.
- Don't use world-writable storage, even temporarily
-
Pretty much every operating system provides a general-purpose
world-readable and world-writable storage area. Although
it is sometimes appropriate to use such an area, you should almost
always find a safer means of accomplishing what
you're setting out to do. If you absolutely must use
a world-writable area, then work under the assumption that the
information can be tampered with, altered, or destroyed by any person
or process that chooses to do so. Ensure that the integrity of the
data is intact when you retrieve the data. The reason that this is so
crucial is that would-be attackers can and will examine every aspect
of your software for flaws; storing important information in a
world-writable storage area gives them an opportunity to compromise
the security of your code, by reading or even altering the data that
you store. If your software then acts upon that information, it does
so under a compromised level of trust.
- Don't trust user-writable storage not to be tampered with
-
For the same reasons as those mentioned in the previous practice,
make absolutely sure not to trust user-writable data. If a user can
mess with the data, he will. Shame on you if you assume that the
information is safe in your user's hands!
- Don't keep sensitive data in a database without password protection
-
Data worth keeping is worth protecting. Know who is using your data
by requiring, at a minimum, a username and password for each user. If
you don't adequately protect that information, then
you have essentially placed it in (potentially) world-writable space.
(In this case, the previous two practices are also relevant.)
- Don't echo passwords or display them on the user's screen for any reason
-
Although most of us who have spent any significant period of time in
the security business would be appalled to see a program that echoes
a user's password on the screen, web sites that do
this are all too common. The principal threat here stems from the
ease with which another user can eavesdrop on the password data as it
is entered (or if it is mistakenly left on the screen while the user
attends to other business). If the purpose of the echoing is to make
sure that the password is entered correctly, you can accomplish the
same goal by asking the user to enter it twice (unechoed) and then
comparing the two strings. Never echo a password
on the screen.
- Don't issue passwords via email
-
This practice reduces the level of
protection to that of the recipient's mail folder.
At worst, this could be very low indeed; even at best, you have no
control over that protection, and so you should assume the worst.
When practical, distribute passwords in person. It's
also possible to develop fairly secure methods to accomplish the task
over telephone lines. Sending passwords over email (or storing them
in any file) is a very unsecure practice. Unfortunately, this is
common practice for web sites that offer a "Forgot
your password?" function of some kind. Our response
to this flawed practice (although it may reduce the number of phone
calls on the respective sites' help desks) is that
it is a disaster waiting to happen! Avoid it if at all possible and
feasible.
- Don't programmatically distribute sensitive information via email
-
Let's not just limit the previous practice to
distributing passwords. Popular SMTP-based email on the Internet is
not a secure means of transmitting data of any kind. Any information
sent over email should be considered to be (potentially) public. At
the very least, you should assume that the security of that
information is beyond your control. For example, many people
automatically forward their email, at least occasionally. Thus, even
if you think that you know where the information is going, you have
no real control over it in practice. Mail sent to a large alias often
ends up outside the enterprise in this way. Consider using
alternative practices, such as sending a message providing the URL of
an access-controlled web site.
- Don't code usernames or passwords into an application
-
Many programs implement a multitiered architecture whereby a user is
authenticated to a front-end application, and then commands, queries,
and so on are sent to a back-end database system by way of a single
canonical username and password pair. This was a very popular
methodology during the late 1990s for such things as web-enabling
existing database applications, but it is fraught with danger. A
curious user can almost always determine the canonical username and
password and, in many cases, compromise the entire back-end program.
It could well be possible to read the access information by examining
the executable program file, for example. In any case, this practice
makes it difficult to change passwords.
When feasible, require that the username and password be typed
interactively. Better yet, use certificates, if they are available.
If you absolutely must use embedded passwords, encrypt the traffic.
- Don't store unencrypted passwords (or other highly sensitive information) on disk in an easy-to-read format, such as straight (unencrypted) text
-
This
practice reduces the security of all of the data to the same level of
protection given the file. If a user, regardless of whether or not he
is an authorized user of your software, is able to read or alter that
information, the security of your application is lost. As with the
previous practice, you should instead use certificates, strong
encryption, or secure transmission between trusted hosts.
- Don't transmit unencrypted passwords (or other highly sensitive information) between systems in an easy-to-read format, such as straight (unencrypted) text
-
This practice reduces the security of all of the data to the same
level of protection given the data stream, which in a subnetwork
without "switched
Ethernet" can be
very low indeed. As with the previous practice, you should instead
use certificates, strong encryption, or secure transmission between
trusted hosts.
Also note that many network protocols, such as FTP and
telnet, send usernames and passwords across the
network in an unencrypted form. Thus, if you are relying on flawed
network protocols, you may be subjecting your software to
vulnerabilities that you aren't aware
of—another reason to not run untrusted software from within
trusted software.
- Don't rely on file protection mechanisms as the sole means of preventing unauthorized file access host-level
-
While it's a good practice to make use of operating
system-provided file access control mechanisms,
don't blindly trust them. The file access control
security of many modern operating systems can be easily compromised
in many cases. The usual avenue of attack involves new security
vulnerabilities for which patches have not been produced (or
applied). Your application software should rely instead on a separate
set of usernames, passwords, and access tables, part of a
securely-designed technique that is integrated into your overall
corporate access control scheme.
- Don't make access decisions based on environment variables or command-line parameters passed in at runtime
-
Relying on environment variables, including those
inherited from a parent process, or command-line parameters is a bad
practice. (It's similar to storing sensitive
information in user or world-writable storage space.) Doing so may
make it possible to gain unauthorized access by manipulating the
conditions under which the application program is invoked. As an
example of a replacement for this bad practice, instead of getting
the user ID from the USER environment variable name, execute the
getuid( ) call from the C library.
- Avoid, if reasonable, storing the application or key data on an NFS-mounted structure
-
Although the utility of Sun's
Network Filesystem (NFS) cannot be
overstated, this facility was never intended to be a secure network
protocol. As with any unsecure network protocol, avoid storing any
sensitive data on NFS-mounted filesystems. Under some circumstances,
NFS security can be defeated, particularly if a critical host is
first compromised. Once again, local servers might be the right
approach in this case. Of course, this requires an additional level
of due care in your programs, to ensure that they are storing any
sensitive data in trustworthy storage areas.
- Avoid, as much as you can, relying on third-party software or services for critical operations
-
Sometimes an "outsourced" solution
is the secure choice, but be aware of any dependencies or additional
risks to confidentiality you create by relying on outside technology
or services. And be sure that you carefully assess the security
aspects of any such third-party solution. Subtle interactions with
third-party code can greatly impact the security of your application.
Likewise, changes to your application and/or upgrades to the
third-party code can affect things in unexpected ways.
|