View all articles

View PDF version of this article


Writing Secure Code

Building security into an application

Ben Lucas
Solien Technology, Inc.

August 2002

Summary: This article will discuss the importance of security as well as common security mistakes and how to avoid them.

Introduction

As security is increasingly important to web applications, developers need to be more aware of common security pitfalls. By keeping these principles at the forefront of any application development, web security can be enhanced leading to better applications.

Why Is Security So Important?

Attacks on websites are more prevalent. In 1995, there were 2,412 incidents reported to CERT; in 2001, 54,658 were reported (http://www.cert.org/stats/cert_stats.html). Information on common exploits is readily available allowing even hackers with a low level of skill to attack a system. These low skill hackers are often referred to as script kiddies because they use tools easily found over the Internet, but they do not really understand how the tools work.

Ultimately, security is important because of its business value. When an attack occurs, the business is directly affected. The business will lose both financial resources and time to resolve the problem. If the attacker gained proprietary information, the financial consequences can be disastrous. For example, the Code Red virus cost an estimated $1.2 billion in damages (http://www.usatoday.com/life/cyber/tech/2001-08-01-code-red-costs.htm).

Finally, security reflects on the quality of the development team. By building insecure solutions, a company will lose credibility with its clients and in the general community.

Types of Attacks

There are three types of attacks that a developer should be familiar with: Denial of Service (DoS), Disclosure, and Integrity.

Denial of service

The purpose of a denial-of-service (DoS) attack is to prevent a system from being used. This can be done by consuming all the resources available on a system or by attempting to crash the system. Although this type of attack is the least damaging to the system, the loss of time as well as revenue, if the service was essential to bringing revenue, can be substantial.

Disclosure

A disclosure attack seeks to gain unauthorized access to data. Depending on what data is discovered, this attack can be very damaging to the business.

Integrity

An integrity attack is looking to damage data by either changing it or deleting it. This is the most damaging attack, and can cause a loss of data. The only way to repair a system is to restore from backups.

Security through Obscurity

Often, security is dropped by the wayside while functions with business value are built. As a project cycle nears completion, the security is never addressed. The assumption is made that since the testing team did not find any security holes, any which may exist would be too difficult to find and exploit. This attitude is an example of the belief in security through obscurity.

Security through obscurity is a fundamental no-no. An analogy would be burying one's life savings in a public park. It might be somewhat 'secure' if you've done a good job hiding it, but that's not real security. On the other hand, a bank vault is very secure because potential criminals know everything there is to know about it, and yet still have a difficult time breaking in. (Schmidt, Jeff. Microsoft Windows 2000 Security Handbook. p. 26)

Although obscurity can improve your defense by making it more difficult to discover a weakness, it should not be relied on as the only defense to an application. Security should always be a consideration when designing and developing software.

General Security Principles

When developing code, there are some general security principles that can be applied to improve the security of the system.

Never Make Assumptions

This principle is the most fundamental security concept to apply. Most of the other security principles are actually variations of this principle.

When building a secure system, never assume anything. Do not trust user input. Do not trust third-party components. Do not trust that the application was installed perfectly.

All of this may seem a little paranoid, but most security vulnerabilities can be traced back to an assumption being made about what could be trusted.

For this reason, the code that you write should be robust enough to handle incorrect user data as well as failure on the part of other software.

Use Least Privileges Needed

In production environments, code should always run with the least privileges needed. Often, this principle is not applied to the development environment. This can lead to defects when the code is moved to a more secure system. In some cases, the defects may be resolved by giving the code more privilege than it should have, introducing new security vulnerabilities.

This principle emphasizes the need to have development systems that mimic the production systems as closely as possible.

Check All Return Codes

Check the values of any return codes to make sure the method has performed its task as requested. Often, it is important to check whether the return value is null as shown in the following example.

[C#]

public static string GetSingleNodeValue(XmlNode node, string xPath)

{

XmlNode localNode = node.SelectSingleNode( xPath );

if (localNode != null)

return localNode.InnerText;

else

return null;

}

Failing Closed

As much as developers strive to write defect-free code, defects can still slip into production code. The "Failing Closed" principle addresses this by stating that when a system fails, it is always better for the system to deny access rather than allow access.

When a system fails closed, it prevents a malicious user from gaining unauthorized access by exploiting a defect.

Test For Security

Finally, testing for security is essential. Unit Tests should specifically try to break the system by using unexpected input and other means. During code reviews, think like a hacker and see if there is an apparent way to exploit the code.

Common Security Mistakes

Poor User Input Validation

The most important security principle is to never make assumptions. In this classic mistake, a developer assumes that the user will enter valid data. This assumption can lead to serious security flaws. In the following example of an authentication method, a user could attack the site fairly easily because the input is not validated.

[C#]

private bool Authenticate(string sUserName, string sPassword)

{

string sSQL = "select * from WebUser where login = '"

+ sUserName + "' and password = '" + sPassword + "'";

SqlConnection conn = new SqlConnection(CONNECTION_STRING);

conn.Open();

SqlCommand command = new SqlCommand(sSQL, conn);

SqlDataReader dr = command.ExecuteReader();

if(dr.Read())

return true;

else

return false;

}

In the above example, a user can actually break into the site with no additional information. If the user gave any user name with the password as " test' or 1=1 or '1'=' ", then the site would decide that he was authenticated. To solve this problem, all single quotes (') could be converted to two single quotes (''). This prevents a user from introducing new code to the SQL command.

Another more effective solution would be to use a stored procedure rather than direct SQL. In .NET, a SqlCommand object could be built to execute that stored procedure. The parameters passed in to the command object would automatically prevent this type of attack. However, one should note that if the stored procedure dynamically built query information, this attack could still be possible. When dealing with SQL, it is best to always verify input.

Clearly, user input must be validated. Expect that users will input the unexpected. When writing web pages, remember that the querystring, Form Post, and cookies are all user input and should be validated.

Bounds Checking

This can be another way to validate user input. Whenever a value is received, it should be checked to verify that it is within acceptable bounds. For example, if somebody is placing a bid on a car, the software should check to make sure the bid is positive. Likewise, arrays should have their bounds checked to avoid any problems with memory usage.

Buffer Overflow

In C++ or other languages that allow a developer to manage memory, the problem of a buffer overflow can occur. A buffer overflow occurs when more data is written to a spot in memory than the memory (i.e., buffer) can hold. If the memory was allocated from the stack, the extra data can overwrite important information on the stack and can allow a potential attacker to execute any code they wish from the context of the running application. Clearly this is a serious security vulnerability as it can compromise the entire system.

For this reason, when writing code in a language such as C++, developers should pay careful attention to the way memory is managed to avoid buffer overflows. The following example shows a buffer overflow.

[C++]

static DWORD OnNotifyPreprocHeaders( PHTTP_FILTER_CONTEXT pfc,

PHTTP_FILTER_PREPROC_HEADERS pHeaders )

{

char sIsaUserName[200] = {'\0'};

DWORD lLen = 201;

pfc->GetServerVariable(pfc, "LOGON_USER", sIsaUserName, &lLen);

return SF_STATUS_REQ_NEXT_NOTIFICATION;

}

The final parameter in GetServerVariable defines how large the buffer is to store the Server Variable. Because lLen tells GetServerVariable that the length is one byte larger than the buffer can actually hold, there is the potential for a buffer overflow.

Overly Complex Code

Writing overly complex code can be a form of "security through obscurity". However, this is a flawed approach to security. Code is usually only viewed by developers and secure code can be written in a clean, easy-to-read manner; the only people really affected by this obscurity are the developers working on the code. Because the complex code is difficult to understand, security flaws are less likely to be discovered.

When security is a consideration, developers must write their code so that it can be understood.

Lack of Proper Authorization

Security does not end once a user is authenticated. For every action a user attempts to make, security checks must be performed to determine whether a user is authorized. Often, developers assume that since they control the links displayed to a user, once the user is authenticated, the user will only be given the option to perform actions to which they have access. However, on a web application the user can modify input such as variables on a querystring. If, for example, a querystring took an Order ID as one of its variables, a user could change that value. If the web application does not verify that the user is authorized to view the data, a malicious user could gain access to another user's sensitive data.

Conclusion

Security has become a crucial part of any application development. Developers need to take security seriously and make security a part of the development process.

Feedback and Support

Questions? Comments? Suggestions? For feedback on this article, please send an e-mail message to ben@solien.com.

More Information

For more information on the contents of this article see the following resources.

Howard, Michael. Designing Secure Web-Based Applications for Microsoft Windows 2000. (Redmond: Microsoft Corporation, 2000).

Schmidt, Jeff. Microsoft Windows 2000 Security Handbook. (Indianapolis: Que Corporation, 2000).

http://www.cert.org