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.
The Limits of TrustEven 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:[3]
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. |
[3] Ken Thompson. "Reflections On Trusting Trust." Communication of the ACM, Vol 27, No 8, August 1984, pp 761-763.
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.
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.
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?
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.
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.
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.
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.
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.
While popular in interactive programs, shell escapes,[4] 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.
[4] A shell escape mechanism is simply an invocation of an interactive shell from within an application. Such mechanisms are traditionally used so that an interactive user of the application can temporarily "escape" out of the application, run a shell command, and then return to the application in its previous state. (Modern windowing environments have largely eliminated the need for them.)
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.
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.
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.
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!
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.)
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.
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.
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.
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.
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.
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"[5] can be very low indeed. As with the previous practice, you should instead use certificates, strong encryption, or secure transmission between trusted hosts.
[5] Although it is a good idea for various security and operational reasons, even a switched Ethernet environment does not remove the dangers of sniffing sensitive data as it traverses a network.
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.
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.
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.
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.
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.