asp:cover story
LANGUAGES: C#
ASP.NET VERSIONS: 1.0 | 1.1
7 Tips for Secure Apps
Use these tips to make sure you don't sacrifice security
in the name of performance.
By John Paul Mueller
Most developers know that security
is a major issue and that their livelihoods will depend on getting the security
of their next application right. There's no lack of pressure to make
applications perform well and still protect data. Unfortunately, performance
often wins out, and in some cases, time is the factor that causes both security
and performance to fall by the wayside. The tips in this article should help
you make your application secure, without giving up much in the way of
performance and without missing those important deadlines.
1. Don't Assume the Environment
is Consistent or Static
Many developers assume that the
programming environment is going to remain consistent forever. They deal in a
static world of absolute facts that never change. However, the world is far
from consistent or static. Some of the worst programming nightmares in history
have occurred because the programming environment changes. Consider the Y2K
debacle or the rise of crackers on the Internet. The real-world environment in
which your application executes is constantly changing - in many cases, not for
the better.
Consider the case of a company that
employed a number of programmers to maintain an application. After some number
of changes, the application did work better. However, because the various
programmers assumed that the company would distribute the application in a
particular environment and that every programmer was conscientious about
security concerns, the application developed a number of security holes, many
of which proved difficult to find. The company finally ended up hiring a
consultant to look through the code and close the security holes.
The best way to avoid assumptions
about the application environment is to make the application flexible. A
flexible application responds to changing threats. For example, you can overcome
many threats through the simple check of incoming data type, content, and
length. In addition, restricting user input to just the information you need
reduces the chance of errant data or a cracker attack. Such applications are
flexible because they don't assume anything about the user or the environment
in which the application will operate.
The moment you choose to focus on a
particular security area because it's currently a threat, an application
becomes vulnerable to attack. Looking for anything that tends to make an
application less reliable, usable, or robust also tends to point out potential
security flaws, even if an issue isn't a security problem now. For example, it
always pays to validate the data going into a buffer because such checks make the
application more reliable. However, such checks take time to write, and they
reduce application performance because they use up processing cycles, so many
developers didn't add them in the past. Today, the buffer overflow is one of
the most pressing security problems vendors face.
2. Perform Range Checks
Determining that the data you
receive is in the correct range is important. For example, if you need a number
between 1 and 5, don't accept the number 6. This simple check extends to other
data types, as well. Make sure you check the length of strings and don't accept
something that is too long. You can perform some range checks by using simple
tag attributes, such as the maxlength attribute for the <input> tag.
Other range checks can rely on code behind; you can check the data once it
arrives at the server. However, the simplest range check is using one or more
validators to ensure the integrity of the data the client sends.
A validator is a special .NET
Framework control. To use it, you fill in two or more properties depending on
the control type. Figure 1 shows a password dialog box that includes two
validators for each of the text boxes. In this case, the first validator
ensures the associated text box contains some text, and the second validator ensures
the input is in the correct format. The ControlToValidate property associates
the validator with the TextBox control, and the ErrorMessage property controls
the error message you see in the figure. When a user fails to provide the
correct input, the validator catches the error and displays the error message.
You don't have to do any programming; just place the validator on screen and
fill out the appropriate properties. In general, you'll rely on four validators
for your applications: RangeValidator, CompareValidator,
RegularExpressionValidator, and RequiredFieldValidator.
Figure 1. Forms commonly contain
multiple validators for each data-entry field. Each validator performs a
specific check on that field to ensure it meets all input requirements and provides
the user with field-specific help should an error occur.
The RangeValidator ensures the
input in a control falls within a range of values. The MinimumValue and
MaximumValue properties contain the limit of values the user can input. You'll
use the Type property to determine what type of data the control will accept.
The RangeValidator accepts common types, including string, integer, double,
date, and currency. If the input doesn't fall within the selected range of
values or is of the wrong type, the control will display an error message.
The CompareValidator accepts two
controls as input and then compares the value of each control. If the two
controls don't match the condition you specify, the CompareValidator displays
an error message. The name of the second control appears in the
ControlToCompare property. The Operator property defines the comparison between
the two controls. For example, if you choose the GreaterThan option, the value
of the control listed in the ControlToValidate property must be greater than
the value of the control listed in the ControlToCompare property. A Type
property ensures the second control contains data of the correct type, but this
is almost superfluous because the two controls won't compare if their types
don't match.
The RegularExpressionValidator uses
an expression to validate the content or format of the input. You'll find that
the Microsoft help topics tend to focus on the format of the expression, as do
the built-in expressions. However, the ValidationExpression property can
contain any regular expression that describes the content you expect. For
example, using a ValidationExpression property value of
[ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz]{3,15} tells the
RegularExpressionValidator to accept only characters as input and to restrict
the length of the input from three to 15 characters. Given the extreme
flexibility of regular expressions, you can define any kind of range check you
might need.
The RequiredFieldValidator is the
easiest validator to understand. If the target control is blank, the validator
displays an error message. Some developers will use an asterisk in place of the
error message and simply display one error message for all required fields.
However, the use of a custom error message for each control means that you can
provide example input for each field.
3. Analyze the Input
Regular expressions and other forms
of analysis can be your best friend for some types of input. For example, I
regularly visit Web sites that don't know whether I've input a telephone number
or a credit-card number or some text that came to mind. Using a regular
expression to test the data won't verify that it's correct, but at least it
will verify that the kind of data is correct. Ensuring the data is in the correct
form automatically reduces the chance that someone will send a virus or hack
into your system by sending incorrect input.
You can accomplish this task using
a RegularExpressionValidator. This control comes with a number of preset
validations, as shown in Figure 2. It's also possible to create your own
regular expressions.
Figure 2. The Regular Expression
Editor contains a number of preset expressions you can use to check input.
These examples also help you create your own regular expressions without spending
a lot of time in the documentation.
Sometimes, you can't use a
RegularExpressionValidator because the data is too complex for a simple
statement or because you need to look for specific information. For example,
you can't validate a credit-card number using a RegularExpressionValidator.
It's also hard to verify free-form data or some types of complex data, such as
part numbers. All of these kinds of input require local validation at the
server. In this case, you can combine a number of checks in a function, such as
the one shown in Figure 3.
private void btnTest_Click(
object sender,
System.EventArgs e)
{
Regex PartCheck; // Checks the
part number format.
// Create the regular
expression.
PartCheck =
new
Regex("[ABCDEFGHIJKLMNOPQRSTUVWXYZ]{3}-" +
"[0123456789]{3}-[0123456789]{5}");
// Check the input
length.
if
(txtPartNum.Text.Length != 13)
{
lblResponse.ForeColor = System.Drawing.Color.Red;
lblResponse.Text =
"Use input form of XYZ-123-01234";
return;
}
// Verify the text is
in the correct form.
if
(!PartCheck.IsMatch(txtPartNum.Text))
{
lblResponse.ForeColor = System.Drawing.Color.Red;
lblResponse.Text =
"Use input form of XYZ-123-01234";
return;
}
// Ensure the user has
actually changed the input.
if (txtPartNum.Text ==
"XYZ-123-01234")
{
lblResponse.ForeColor = System.Drawing.Color.Red;
lblResponse.Text =
"Please change the part number.";
return;
}
// Check for part in
database
// Perform other
checks.
}
Figure 3. Use
customized checks when a RegularExpressionValidator won't work for data
validation. At a minimum, always check the input length and data type (or
complex type).
The code shown in this listing begins
by creating a Regex object. This object checks the form of the data input. In
this case, the input consists of three letters, a dash, three numbers, a dash,
and five more numbers.
At this point, the code performs a
number of data checks. The first check is the length of the input. The reason
you perform this check first is to reduce the risk of someone sending a script
as part of the data. The next check validates the form of the data. Finally,
the code ensures the user didn't just send the sample data as input.
At this point, the data is
relatively safe, but you haven't verified that it's actually good data. You'd
need to check for the part number in the database or perform some other
validation checks. The idea is to move from general checks to specific checks
in order to prove the data is good (or at least as close as possible) before
you use it for anything. Although this series of checks borders on the
paranoid, you'll find that paranoid developers tend to have fewer security
problems.
4. Perform Double Checks
You can limit the length of input
using a simple HTML tag attribute. For example, take a look at the following:
<input type=text name="MyCheck" maxlength=40/>
This tag limits the size of the
input to 40 characters. Any legitimate user will type 40 characters, discover
there's no room for additional text, and leave the input as is. A cracker, on
the other hand, might be tempted to exceed the 40-character limit.
The double check comes at the
server with your ASP.NET code. If you suddenly discover that this 40-character
input has 500 characters in it, you know someone has been tampering with the
input. This activity is suspicious, and you need to check it out. No, this
check won't prevent someone from attempting to input bad data, but it will
alert you to the tampering. This kind of security check is a lot more helpful
than you might think, because it looks for the unexpected.
5. Use Discrete Inputs
One of the main problems with most
Web forms is that the user faces a blank text-input field that could contain
anything. Figuring out what to type takes time that the user might not have.
Most developers like to use text-input fields because they provide freedom of
choice, but users often see text-input fields as problematic. Even if the developer
provides a sample value and the user honestly is trying to use the form
correctly, there's still a lot of room for interpretation. Users ask questions
such as, "Do I need to type a number or a word in this field?"
In many cases, you can replace the
general input with discrete input. For example, a list box contains specific
allowable values that make the choices clear to the user. The form is more
accessible because the user doesn't have to figure anything out; all the
correct answers appear directly on the form. A user also can fill the form out
faster.
From a security perspective, the
programmer wins in a big way. When you use discrete input such as a list box,
the cracker can't provide a handy script in place of the simple text you were
expecting. Discrete inputs require less validation and less code, so using
discrete inputs is better for the developer and the user.
6. Stem the Tide of Leaking
Information
Most employees at your company
aren't trustworthy. It's not that they're spies for someone else or
disgruntled; it's that they simply don't think about the consequences of
placing certain information on a Web site or understand computers well enough
to know they're letting the information out. Consider the case of a company
that handles sensitive information. An employee let some of this information
out by mistake - an honest error. Because the application assumed the employee
understood the consequences of making this information available, the employee
lost a job, and the company lost clients. If the application developer had
assumed that employees are human and make mistakes, the data might not have
appeared on the Web site. The employee might have gotten a reprimand for not
following procedure, but he or she still would have a job, and the company
wouldn't have lost clients.
Many developers discount the
problem of data leaks because they think they have followed every security
procedure. However, data leaks occur not because of a programming problem but
because of an implementation problem. The reason you should care about the data
output of your application is that people make mistakes, and your application
can help protect them from errors.
Displaying any information at all
on a Web site causes an information leak for your organization. In another
case, an employee posted a list of contact names and e-mail addresses on a Web
site without considering the effects of providing such a list. A cracker used
the list to perform social engineering at the company. By using the information
provided on the Web site, the cracker was able to obtain user-name and password
information from several employees. The employees felt the cracker must have a
legitimate need for the information because he or she knew so much about the
people working there. Of course, because the purpose of a Web site is to
provide information to viewers, your application wouldn't be worth very much
without at least a small information leak. In addition, some kinds of
information leaks are actually good for your organization, such as when you
announce a new product.
Part of your responsibility as a
developer is ensuring your design doesn't leak private data. For example, make
sure your template contains only the information it needs to contain. Of
course, the template still has to provide functionality, so you can't leave it
blank. Make sure that you don't confuse protected company information with
essential user information. You still have to make the Web page accessible, and
it still requires good help to avoid confusing the user.
Data-leak protection can take other
forms. For example, your organization might rely on a database to supply
information for Web pages. At some point, your program will need to access the
required database record, extract the data, apply it to a page, and output the
result. This setup provides a perfect opportunity to scan for forbidden words -
such as the name of a new secret project - during the extraction phase, using
regular expression parsing. Obviously, this form of data-leak protection won't
fulfill every need; a smart user that is determined to leak information will
find some other words to do it. This limitation is the same reason spam filters
are only partially successful.
There is one last thing to consider
about data leaks: All of this monitoring does have an effect on application
performance. Consequently, you might have to weigh the security and privacy
benefits of output monitoring against the performance penalty of doing it. Some
types of businesses, such as financial institutions, probably should monitor
everything and make up the performance loss in other ways. However, if your
organization works mainly with public data, you probably can use less
monitoring to achieve better performance.
7. Use Performance Counters to
Your Advantage
Some people assume that performance
counters are only useful for monitoring how fast a system performs a given
task. However, performance counters can be the best security investment you
ever made, because they help you monitor the condition of your application. You
can use a performance counter to count anything, including the number of errors
your application experiences because of incorrect password entries. Too many
password errors could signal a cracker's attempt to infiltrate your system.
Performance counters also can measure application traffic and make it possible
to discover trends in application usage. Analysis helps you learn about
problems, such as a Distributed Denial of Service (DDOS) attack, before they
really become problems.
The reason analysis is important is
that you can prevent only so many kinds of security problems. Given enough
time, any good cracker can overwhelm the fixed fortifications you provide for
your application, which means you must include some form of monitoring, also.
Detecting security breaches is at least as important as preventing them in the
first place.
Unfortunately, developers didn't
always have access to the simple monitoring technique demonstrated in this tip.
Creating a performance counter in the Win32 API environment is nothing short of
a nightmare, so many developers avoid performing this task. However, the .NET
environment makes the task of creating a custom performance counter relatively
easy. All you need to do is create a custom performance counter that tracks the
number of bad requests to your application. When the number reaches a specific
threshold, you know that something is wrong - it could be a DDOS attack. Figure
4 shows an example of the performance-counter approach.
// A collection of counters.
CounterCreationDataCollection CounterCollect;
// The error counter.
CounterCreationData ErrorCount;
// Determines random error.
Random EventVal;
// Error performance counter.
PerformanceCounter PerfCount;
public frmMain()
{
// Required for Windows
Form Designer support
InitializeComponent();
// Create the
collection and error counter.
CounterCollect = new
CounterCreationDataCollection();
ErrorCount =
new
CounterCreationData(
"Error_Count",
"Contains the application error
count.",
PerformanceCounterType.RateOfCountsPerSecond32);
// Add the counter to
the collection.
CounterCollect.Add(ErrorCount);
// Create a custom
counter category.
PerformanceCounterCategory.Create(
Application.ProductName,
"Error checking
counter.",
CounterCollect);
// Create the
performance counter.
PerfCount = new
PerformanceCounter(
Application.ProductName,
"Error_Count",
false);
// Create the random
number generator.
EventVal = new
Random(DateTime.Now.Second);
}
private void btnTest_Click(
object sender,
System.EventArgs e)
{
// Set the number of
events per second.
EventGen.Interval =
Convert.ToInt32(1000
/ txtTimerVal.Value);
// Start the timer.
EventGen.Start();
}
private void EventGen_Tick(
object sender,
System.EventArgs e)
{
// Generate a random
error event.
if (EventVal.Next(5)
<= 1)
PerfCount.Increment();
}
private void frmMain_Closing(object sender,
System.ComponentModel.CancelEventArgs e)
{
// Destroy the counter.
PerformanceCounterCategory.Delete(
Application.ProductName);
}
Figure 4. Performance
counters can do more than just help you track performance. You also can use
them for error trapping and making security checks. Using performance counters
adds automation to the security process.
I used a standard Windows application for the example to
make it easier to test, but the same technique works fine in a component or any
other kind of application you want to create. The constructor begins by
initializing the counter. This example shows a very simple counter. The code
begins by creating a counter and a counter collection to hold it. It then uses
the PerformanceCounterCategory.Create method to add the collection to Windows.
The collection can contain any number of counters, and you can create counters
of various types. You can read more about the various performance counter types
at http://msdn.microsoft.com/library/en-us/cpref/html/frlrfSystemDiagnosticsPerformanceCounterTypeClassTopic.asp.
Creating a performance counter for
Windows doesn't provide access to it. The code creates a new PerformanceCounter
object using the name of the counter category and the individual counter.
Notice that you must set the ReadOnly argument to False, or you won't be able
to generate new data for the counter.
The one element in the constructor
that you don't need to create, in most cases, is the Random object, EventVal.
This object generates random errors in this example. In a production
environment, you'd only want to know about the real errors.
The btnTest_Click method sets the
timer interval and starts the timer. Once the timer starts, it generates ticks
at the rate specified by the Interval property. Each call to the EventGen_Tick
event handler is the result of a tick. The code checks the current random
number value. If it's less than or equal to 1, the code increments the
performance counter using the PerfCount.Increment method.
Because this is a temporary
counter, you should destroy it before you exit the application. The
frmMain_Closing method accomplishes this task. All you need to supply is the
name of the counter category.
The sample code in this article is
available for download.
John Mueller is a
freelance author, technical editor, and consultant who has produced 60 books
and more than 200 articles to date. The topics range from networking to
artificial intelligence and from database management to heads-down programming.
His most recent book is.NET Development
Security Solutions (Sybex, ISBN 0-7821-4266-4). His technical-editing
skills have helped at least 32 authors refine the content of their manuscripts.
You can reach John on the Internet at mailto:JMueller@mwt.net,
and his Web site at http://www.mwt.net/~jmueller/.