Secure Coding Practices This section delves into the more technical aspects of security in the code used to implement an application, and provides guidelines to develop an enforcement process for secure implementation. The potential for vulnerabilities in an application is reduced by a strong design, but the implementation of the application seals its fate. Hard work poured into a secure design becomes inconsequential if the implementation is poorly done. It is also important to understand that the inclusion of security-related technologies or design methods does not necessarily imply or guarantee any level of security within an application. The implementation of an application and any security technology used is one of the final components that brings a high level of reliability. Analysis in the implementation phase is the responsibility of both developers and development managers. Developers are responsible for implementing the design well, whereas managers are responsible for setting forth the process that ensures a good implementation. This can be done via standardized procedures that include documented development and coding standards, design and code reviews, and developer training with regard to security in application development. These procedures benefit the developers and applications regardless of which languages are used or the type of application developed. The most commonly used languages today are the C programming language, Java, and scripting languages such as Perl and the UNIX shells. Each of these languages and environments can be used improperly to compromise the security of an application and the system on which it runs. The following demonstrates security issues with these languages in relation to the vulnerabilities outlined earlier in the chapter. However, this section is not a checklist for developers to follow. Instead, the development of a security-focused thought process allows for an arguably stronger coding practice. Pitfalls by the C The C programming languages, which include C, C++, and object-C, are the most commonly used languages and can be dangerous in unsure hands. They provide the developer with the ability to manipulate and access many parts of the system, such as memory, files and devices. This is a great strength of the C languages, but danger arises when the developer makes mistakes. C provides a high level of access to the underlying operating system, and there are few checks and balances to protect the developer. If the developer mistakenly writes data to the wrong device or memory location, the C program will do whatever the developer writes, regardless of the data or destination. The first vulnerable area often associated with C is the buffer overflow. The following sample code demonstrates a very basic overflow: char string[10]; strcpy(string, "AAAAAAAAAAAAAAA"); /* 15 "A" characters */ Here, 15 "A" characters are copied into the memory area for a variable string, which is declared to be a static 10-character array. The strcpy() function does exactly as directed with no regard for the size of the data being copied or the location to which it goes. A buffer overflow occurs when the 11th element is copied into the memory location, immediately following the location of the 10th element of the variable string. Now apply this principle to any input data that comes from an external source, replacing the string of "A" characters. This allows attackers to control the effects of the overflow. The strcpy() function is one of several functions in C that do not perform any bounds checking and allow arbitrarily sized buffers to be copied. Other functions to avoid are gets(), strcat(), sprintf(), and the scanf() family of functions. There are updated versions of some of these functions that allow the lengths copied to be specified. These are strncpy(), strncat(), snprintf(), and fgets(). These modified functions copy only up to the number of characters specified by the length parameter. At most, length characters are copied from source into destination: strncpy(destination, source, length); Tip When using the "n" versions of the string manipulation functions strncpy(), strncat(), snprintf(), and fgets() be sure that the length is not larger than the destination string, rather than the source string. The buffer can be overrun if the length value is larger than the size of the destination buffer. When using pointers to buffers, instead of statically declared buffers, you need to allocate enough memory to store the values being copied. Use the memory manipulation functions, which allow you to specify length. Tip When allocating memory for string data, do not forget to add 1 to the total length, in order to accommodate for the NULL terminating character. Without a NULL terminator, the data in memory directly after the last character of the string might be considered part of the string. These functions are not the only places where buffer overflows occur. Be sure to check that information read, copied, or written to any memory location or assigned to a variable will fit, or that the destination allocated has enough storage space. Tip To avoid buffer overflows in your code, be sure to validate input. Check the size of the data and the storage location and use manipulation functions that generate developer-specified amounts of information instead of arbitrarily long chunks of data. Race conditions add a level of complexity to using C code. Race conditions can be exploited in two aspects of C code creation sequencing and protection. Sequencing refers to the order in which events occur in an application. Race conditions can result from sequencing variations between dependent events, when no checking is done between the events. This often signifies a shortcoming in any error-checking and validation routines used. If two functions normally run sequentially and the second function assumes that the results of the first are valid, then the possibility for a race condition exists. Elevated privileges, discussed in further detail in the "Operating System Interactions" section earlier in this chapter, are often targets of attack. Organization, combined with sequencing and error checking, minimizes the possibility for race conditions. This is a bad implementation that creates a race condition: increase_privs(); ... value = special_app_function(); /* requires privileges */ other_unreleted_function(); /* does not require privileges */ other_unreleated_function2(); /* ditto */ special_dependent_function(value); /* requires privileges */ ... exit(); Here, a couple of unsafe practices occur. The privileges of the application are elevated early in the application, but not used until later. They are also never relinquished, so most functionality executes with higher-than-needed privilege levels. Finally, the race condition is created through poor organization dependent functions do not occur near each other. An example that solves these problems is increase_privs(); value = special_app_function; /* requires privileges */ if (!validate_function(value) /* assure the safety of the value */ { do_error_processing(value); /* do something intelligent with the error */ } special_dependent_function(value); /* requires privileges */ decrease_privs(); /* no longer need privileges */ other_unreleted_function(); /* does not require privileges */ other_unreleated_function2(); /* ditto */ Note the special validation and error processing routines that are used before passing the value to another function. Tip Organize functionality and combine it with validation to ensure that expected information is not compromised between dependent events. Many race conditions exist as the result of poor temporary file usage. When these files are created, they should be protected against external attack during operation. UNIX and Windows allow the developer to set the permission bits and operational flags when creating a file. Permissions should disallow access to anyone but the owner of the process. When creating the file using the open() call, set the O_EXCL and O_CREAT flags, which cause the function to return an error if the file you are attempting to create already exists. Because it is a temporary file, it should not exist prior to the need for it. If the file exists, this is a possible sign of attack. When using these methods, it is also important to check the return values of the functions and to clean up any files in the event of error conditions. The following example shows the syntax to open a file or create one, if it does not already exist with permissions that allow only the creator to read, write, and execute it using the S_IRWXU mode macro. open("filename", O_CREAT | O_EXCL | O_WRONLY, S_IRWXU); The call will fail in the event that the file exists already because of the O_EXCL flag. Also, note the unsecure filename. The static naming convention used increases the risk of attack dramatically because one component of the attack is already provided. Due to the increased presence of temporary file race conditions and the associated insecurities, several operating systems have specific functions to create temporary files in a secure manner. The mkstemp() and mktemp() functions have been written and rewritten to solve the protection issues and the predictability problem discussed in the "Temporary Storage" vulnerability section. Developers can also use an operating system's built-in file-locking capabilities to control access to the files. These methods control access in the fundamental kernel components. Tip When using temporary files, randomize the filenames, set strong permissions, and organize the creation, use, and removal of the files to minimize the possibility of attack. Another component that increases reliability and can influence the security of an application is the return value. While it might seem obvious, it is important to stress the necessity of validating return values of functions. Functions often execute serially and rely on the results or data from a previous function. By checking the return value of previous functions, the dependent function is protected from executing with invalid data. Even when the events are not attacks, recovering from anomalous conditions increases the robustness of the application. This example demonstrates a poor implementation that fails to check return values: n = do_string_check (string, valid_characters); /* function returns an int */ if (n == GOOD_RETURN) { process_string(string); } Here, the implementation is weak because the negative case, a bad return, is never handled. A better implementation is n = do_string_check(string, valid_characters); /* function returns an int */ if (n != GOOD_RETURN) { special_error_processing_routine(n); /* bad value, do something */ } process_string(string); The negative return is handled by the-error processing routine, which can exit the program, request a new string, or convert the return value into a valid parameter. If the return value is good, it goes to the process routine. Tip The creation of reusable event and error routines provides a standard mechanism by which all applications react to various attacks and issues. Ideas for these routines include common validation methods for string and numeric values, wrapper functions to perform integrity checks, and protection mechanisms that validate variables and memory locations. Always check and process the return value of a function. The next bit of detail involves the use of sensitive information within the application, including passwords, encryption, or any other private information. As mentioned previously, all program information exists in areas of the common pool of memory that can be subject to reading and modification by external procedures. It is beneficial to clear the memory when the information is no longer in use, in order to avoid revealing information during an attack. The most common and sufficient method to clear data, this is typically referred to as zeroing out memory. When stored information is no longer needed, the storage locations should be overwritten with zeros or random data to prevent an attacker from recovering the information via memory or core dumps. This procedure becomes particularly important when encryption is in use. The keys used to encrypt and decrypt messages are the most important pieces of a cryptographic system, and everything possible to protect them needs to be done. These guidelines exemplify some of the common issues that arise in C-based applications. C is a very popular and powerful language that allows provides great flexibility to the developer, and care should be taken with its use. A Perl of an Application Perl is an interesting beast that combines many of the benefits of a structured programming language, like C, with the flexibility and integration of a UNIX shell. Perl allows the developer to create procedures or subroutines, define variables, and utilize applications and commands available with the operating system. These capabilities, and its strength with regular expressions and parsing, give Perl a strong presence in Web applications, system administration, and automation. Perl programs are not generally susceptible to buffer overflows because of the weakly typed nature of its variables and declarations. Unlike C, wherein variables and memory need to be defined as a particular storage class and memory must be allocated for them, Perl does everything automatically and treats everything as string data. Take the following example: #!/path/to/perl $one = 1; $one_s = "1"; # No different than $one $two = $one + $one_s; # the result is 2, or "2", which are equivalent In C, the variable $one would likely be declared an integer and $one_s, a string. The addition of the two elements would also result in an erroneous value. Perl does not differentiate between the different types, so $two is assigned 2, or "2" they are equivalent in Perl. The language also does not fall prey to the memory allocation requirements that other languages exhibit. The following example is completely acceptable in Perl: #!/path/to/perl $var1 = "AAAAA"; $var2 = "BBBB"; $var2 = $var1; # $var2 becomes "AAAAA"; $var2 takes on the new value of five "A" characters. All variables are dynamically allocated; there is no concept of preset storage space that could be overflowed. Perl is susceptible, however, to race conditions and the vulnerabilities associated with the execution of external programs. Care should be given to the sequencing of functions. Input validation is equally important in Perl, in order to prevent the exploitation of external applications. Perl supports the capability to open files, similarly to C and other languages, therefore the use of temporary files should incorporate appropriate permissions and creation flags. The use of Perl in Web-based CGI programs is also extremely popular. The greatest risks associated with its use in this environment occur during input validation and execution of external system programs from within. To protect the application, several precautions can be taken. Using the taint-check mechanisms of Perl, any variable set outside of the program will not be passed to any program run by the application. Any variables set by the tainted variable become tainted. Taint-check mode is particularly useful for avoiding vulnerabilities, wherein unchecked user variables are passed surreptitiously to programs called from the Perl application. To initialize version 5 of Perl in taint-check mode, use the following script header: #!/path/to/perl T # Run in taint-check mode The next precaution is to parse input values to remove meta-characters and unwanted values. This helps protect against attacks that exploit parameter-passing to shells and other applications. The following example shows a simple routine that scans an input string for any meta-characters that might be interpreted by a program: $unclean_input = &get_HTML_forms_response(); if($unclean_input =~ tr/;|`!#$&*()[]{ } <>:'"//) { # Print out some HTML here indicating failure &do_some_error_reporting(); } In this case, the routine reports an error if a meta-character is found. Alternative methods replace meta-characters, or only continue if no meta-character is found. A final precaution is the use of a shell to run other applications. As with the UNIX system() call and the Windows exec() call, the system() call in Perl allows the developer to run another application. The exec() call in Perl functions like that call in UNIX the running process is replaced by the program indicated. These functions can be particularly dangerous when used in an environments that allow user input, such as CGI programs or system utilities. If input validation does not occur, the application can be exploited to execute arbitrary programs that can affect the system. The following example demonstrates the insecurities of using system() with nonvalidated input. Assume the user supplied the string username ;/bin/rm rf / that became assigned to the variable $input: system("ecommerce_app $input"); This effectively translates to /bin/sh ecommerce_app username ; /bin/rm rf /. Assuming that the program is running with privileges, the program will execute a shell to run the e-commerce application; hit the shell semicolon, which is the command separator in a shell; and then run rm rf, which erases the entire file system. Mi Java Es Su Java Java is a relatively recent invention in the world of distributed Internet computing. It brings to fruition the concept of platform-independent code. Java works by writing code and compiling it into a special format that is then run on Java Virtual Machines. The Virtual Machine (VM) is platform specific, but the code that runs on it is not. Java allows Web browsers and remote systems to run more complex and interactive applications. The Web browser accesses a Web site and receives a Java applet from the server. This applet then runs in the Web browser and can communicate with the originating Web server. When introduced, Java transformed static Web pages into dynamic and flowing applications. Since the early days, the use of Java has expanded into many different distributed application areas such as network management, embedded Internet appliances, and other utility functions. Java is a fine example of a language whose developers considered security in the early design stages. The initial versions of Java had a well-documented security architecture, called the sandbox, that prevented the Java applet or application from accessing system resources. As use of Java began to expand, the need arose for access to system resources outside of the sandbox. The first version of the Java Development Kit provided the use of signed applets. The model describes an applet that is digitally signed to verify its creator. When the digital signature is verified, the applet is then trusted by the local system, which allows the applet access to other system resources. This digital signature method involves a fair amount of complex programming to work correctly. It is also important to note that this security model of a digitally signed Java applet is flawed. Anyone can sign an applet. An malicious applet can be signed by the attacker and downloaded by the Web browser. The Web browser effectively verifies that the malicious applet is indeed written by the attacker, and then happily executes it, to whatever result is programmed in it. The current and second iteration of the Java security architecture is much more powerful and flexible than earlier versions. This allows Java to enter many areas of application development previously beyond its capabilities. The new Java security architecture uses easily definable security policies and access control methods that allow an applet or application to access specific resources to varying degrees. In relation to the guidelines presented here, Java designers analyzed the various interactions and vulnerabilities present with distributed Internet applications, and arrived at a model that provides high security with extreme flexibility. The use of Java security policies requires a fair amount of reading and understanding that is beyond the scope of this chapter. For complete documentation on Java and its APIs, see http://java.sun.com. The Shell Game and UNIX UNIX shells form the basis of user interaction with a UNIX system. Shells are command-line interpreters that support some level of automation and programming in the form of shell scripts. These scripts are often used to automate system tasks, perform repetitive operations, and run CGI Web applications. As with Perl, areas of potential risk are input validation, race conditions, interaction with external files and programs, and the organization of functionality. In UNIX, privileged operations can be run by a privileged user, or they can be set to run as a privileged user. There are subtle differences between the two methods. All files in a UNIX system, including applications, have a set of attributes that include user and group ownership, and a set of permissions flags. Combined, they allow file access to be strictly controlled. Normal applications are owned by a user and, depending on the access permissions, they might be run only by the owner, by a group, or by anyone on the system. The applications inherit the privileges of the user who runs them. An application that requires root privileges can be run by a non-root user, but, at those points where higher privileges are required, it will fail. To overcome this and allow normal users to access certain privileged functions, UNIX provides the SetUID and SetGID flags. When enabled, they cause the application to run as the owner or group for that application they set the User ID (UID) of the application to whomever owns it. Many CGI and system programs require access to system resources and are SetUID root. This applies to compiled programs, such as C programs, and scripts, including Perl and the UNIX shells. Experienced UNIX users and developers often warn about the dangers of SetUID shell scripts that provide root privileges. As discussed earlier, input validation and race conditions are easily exploited when the script is not protected properly. When running a script as a privileged user, there is no easier way to hand over the keys to the castle than a weak shell script. Such scripts are particularly dangerous when programmed without security measures because a shell is interactive by nature. Users supply input, and the shell performs a function. Perl has many built-in checks and balances that allow safer SetUID usage. Internet Appliances Internet Appliances are those systems and devices whose entire purpose is Internet computing. All the design guidelines, programming language considerations, vulnerabilities, and operating paradigms discussed in this chapter are directly relevant to Internet Appliances. Internet Appliances often use common operating systems, applications, and methods to accomplish their goals. If the application in development follows this path, pay special attention to all of the information presented here. Some Internet Appliances are developed from scratch, incorporating only newly developed designs and technologies. Assessing security risk for these systems requires extra diligence. It is especially important to integrate security into a design process when starting from scratch. |