Michael Howard and Keith Brown
This article assumes you are familiar with C++, C#, and SQL
Summary: When it comes to security issues, there are many situations that can lead to trouble. You probably trust all code running on your network, give all users access to important files, and never bother checking to see if the code on your machines has changed. You may also not have antivirus software installed, fail to secure your own code, and give too many accounts too many permissions. You may even be careless in using a number of built-in functions that allow malicious intrusions, and you may leave server ports open without any monitoring. Obviously, we can give many more examples. What are the really important issues (i.e., the most dangerous errors that should be given immediate attention to avoid compromising your data and systems)? Security experts Michael Howard and Keith Brown offer ten tips to help you out.
-------------------------------------------------- ----------------------------------
Security issues involve many aspects. Security risks can come from anywhere. You may have written ineffective error handling code or been too generous when granting permissions. You may have forgotten what services are running on your server. You may accept all user input. And so on. To give you a head start on protecting your computer, network, and code, here are ten tips you can follow for a more secure network strategy.
1. Trusting user input puts you at risk.
Even if you don’t read the rest, remember this: “Don’t trust user input.” The problem arises if you always assume that the data is valid and not malicious. Most security vulnerabilities involve attackers feeding maliciously written data to servers.
Trusting the correctness of input can lead to buffer overflows, cross-site scripting attacks, SQL insertion code attacks, and more.
Let’s discuss these potential attack vectors in detail.
2. Prevent buffer overflow
When an attacker provides data length that is greater than the application expects, a buffer overflow occurs, and the data overflows into the internal memory space. Buffer overflow is primarily a C/C++ problem. They're a threat, but usually easy to fix. We have only seen two buffer overflows that were not obvious and difficult to fix. The developer did not anticipate that the externally provided data would be larger than the internal buffer. The overflow causes the corruption of other data structures in memory, which is often exploited by attackers to run malicious code. Array index errors can also cause buffer underflows and overruns, but this is less common.
Take a look at the following C++ code snippet:
void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) {
char cBuffDest[32];
memcpy(cBuffDest,cBuffSrc,cbBuffSrc);
}
What's the problem? In fact, there is nothing wrong with this code if cBuffSrc and cbBuffSrc come from a trustworthy source (such as code that does not trust the data and therefore verifies its validity and size). However, if the data comes from an untrusted source and has not been verified, then an attacker (untrusted source) can easily make cBuffSrc larger than cBuffDest and also set cbBuffSrc to be larger than cBuffDest. When memcpy copies the data into cBuffDest, the return address from DoSomething is changed, and because cBuffDest is adjacent to the return address on the stack frame of the function, the attacker can perform some malicious operations through the code.
The way to compensate is not to trust the user's input, and not to trust any data carried in cBuffSrc and cbBuffSrc:
void DoSomething(char *cBuffSrc, DWORD cbBuffSrc) {
const DWORD cbBuffDest = 32;
char cBuffDest[cbBuffDest];
#ifdef _DEBUG
memset(cBuffDest, 0x33, cbBuffSrc);
#endif
memcpy(cBuffDest, cBuffSrc, min(cbBuffDest, cbBuffSrc));
}
This function demonstrates three properties of a properly written function that can reduce buffer overflows. First, it requires the caller to provide the length of the buffer. Of course, you can't blindly trust this value! Next, in a debug build, the code detects whether the buffer is actually large enough to hold the source buffer. If not, an access violation may be triggered and the code may be loaded into the debugger. When debugging, you'll be surprised how many bugs you find. Finally and most importantly, calls to memcpy are defensive in that they do not copy more data than the target buffer can hold.
As part of the Windows® Security Push at Microsoft, we created a list of safe string handling functions for C programmers. You can find them in Strsafe.h: Safer String Handling in C (English).
3. Prevent cross-site scripting
Cross-site scripting attacks are a unique problem of the Web. They can harm client data through a hidden vulnerability in a single Web page. Imagine the consequences of the following ASP.NET code snippet:
<script language=c#>
Response.Write("Hello," + Request.QueryString("name"));
</script>
How many people have seen similar code? But surprisingly it has problems! Typically, users will access this code using a URL similar to the following:
http://explorationair.com/welcome.aspx?name=Michael
The C# code assumes that the data is always valid and just contains a name. However, an attacker could abuse this code by providing script and HTML code as names. If you enter the following URL
http://northwindtraders.com/welcome.aspx?name=<script>alert(' Hello!');
</script>
You will get a web page with a dialog box saying "Hello!" You might be saying, "So what?" Imagine that an attacker could trick a user into clicking a link like this, but the query string contained some really dangerous script and HTML, thereby obtaining the user's cookie and sending it to a website owned by the attacker; now the attacker has access to your private cookie information, or worse.
To avoid this, there are two ways. The first is to distrust the input and strictly limit what the username contains. For example, you can use regular expressions to check that the name only contains a common subset of characters and is not too large. The following C# code snippet shows how to accomplish this step:
Regex r = new Regex(@"^[w]{1,40}$");
if (r.Match(strName).Success) {
// good! String is ok
} else {
// not good! Invalid string
}
This code uses regular expressions to verify that a string contains only 1 to 40 letters or numbers. This is the only safe way to determine whether a value is correct.
There is no way HTML or script can fool this regular expression! Don't use regular expressions to look for invalid characters and reject requests if such invalid characters are found, because it's easy to miss something.
The second precaution is to HTML-encode all input as output. This reduces dangerous HTML tags to safer escape characters. You can use HttpServerUtility.HtmlEncode in ASP.NET, or Server.HTMLEncode in ASP to escape any potentially problematic strings.
4. Don't request sa permissions
The last input trust attack we'll discuss is SQL code insertion. Many developers write code that takes input and uses that input to build SQL queries that communicate with a backend data store such as Microsoft® SQL Server™ or Oracle.
Take a look at the following code snippet:
void DoQuery(string Id) {
SqlConnection sql=new SqlConnection(@"data source=localhost;" +
"user id=sa;password=password;");
sql.Open();
sqlstring= "SELECT hasshipped" +
" FROM shipping WHERE id='" + Id + "'";
SqlCommand cmd = new SqlCommand(sqlstring,sql);
•••
This code has three serious flaws. First, it establishes a connection from the Web service to SQL Server as the system administrator account sa. You'll soon see the pitfalls of this. Second point, pay attention to the smart practice of using "password" as the password for the sa account!
But the real concern is the string concatenation that constructs SQL statements. If the user enters 1001 for ID, you get the following SQL statement, which is completely valid.
SELECT hasshipped FROM shipping WHERE id = '1001'
But attackers are much more creative than that. They would enter a "'1001' DROP table shipping --" for the ID, which would execute a query like this:
SELECT hasshipped FROM
shipping WHERE id = '1001'
DROP table shipping -- ';
It changes the way the query works. Not only does this code try to determine if something has been shipped, it also proceeds to drop (delete) the shipping table! Operator -- is the comment operator in SQL, which makes it easier for attackers to construct a series of valid but dangerous SQL statements!
At this time, you may wonder how any user can delete the table in the SQL Server database. Of course you are right, only administrators can do such a job. But here you are connecting to the database as sa, and sa can do whatever he wants on the SQL Server database. Never connect to SQL Server as sa from any application; the correct approach is to use Windows Integrated Authentication, if appropriate, or connect as a predefined account with appropriate permissions.
Fixing SQL insert code problems is easy. Using SQL stored procedures and parameters, the following code shows how to create such a query - and how to use regular expressions to confirm that the input is valid, since our transaction specifies that the shipment ID can only be 4 to 10 digits:
Regex r = new Regex(@"^d{4,10}$");
if (!r.Match(Id).Success)
throw new Exception("Invalid ID");
SqlConnection sqlConn= new SqlConnection(strConn);
string str="sp_HasShipped";
SqlCommand cmd = new SqlCommand(str,sqlConn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@ID",Id);
Buffer overflows, cross-site scripting, and SQL insertion code attacks are all examples of trusted input problems. All of these attacks are mitigated by a mechanism that considers all input to be harmful unless proven otherwise.
5. Pay attention to the encryption code!
Let’s take a look at something that might surprise us. I found that more than thirty percent of the security code we examined had security vulnerabilities. Perhaps the most common vulnerability is your own encryption code, which is likely to be vulnerable. Never create your own encryption code, it's a fool's errand. Don't think that just because you have your own encryption algorithm that others can't break it. Attackers have access to debuggers, and they also have the time and knowledge to determine how systems work - often breaking them within hours. You should use the Win32® CryptoAPI, the System.Security.Cryptography namespace provides a number of excellent and tested encryption algorithms.
6. Reduce the possibility of being attacked.
If more than 90% of users do not request it, a feature should not be installed by default. Internet Information Services (IIS) 6.0 follows this installation recommendation, which you can read about in Wayne Berry's article "Innovations in Internet Information Services Let You Tightly Guard Secure Data and Server Processes," released this month. The idea behind this installation strategy is that you won't pay attention to services that you are not using, and if those services are running, they could be exploited by others. If a feature is installed by default, it should run under the principle of least authorization. That is, don't allow applications to run with administrator privileges unless necessary. It's best to follow this advice.
7. Use the principle of least authorization
Operating systems and common language runtimes have a security policy for several reasons. Many people assume that the main reason this security policy exists is to prevent users from intentionally causing harm: accessing files they are not authorized to access, reconfiguring the network to suit their needs, and other egregious behavior. Yes, this type of insider attack is common and needs to be guarded against, but there's another reason to stick to this security strategy. That is, building defensive barriers around the code to prevent users from wreaking havoc on the network through intentional or (as often happens) unintentional actions. For example, an attachment downloaded via e-mail, when executed on Alice's machine, is restricted to resources that Alice can access. If an attachment contains a Trojan horse, a good security strategy is to limit the damage it can do.
When you design, build, and deploy server applications, you cannot assume that all requests come from legitimate users. If a bad guy sends you a malicious request (hopefully not) and your code behaves badly, you want your application to have every possible defense in place to limit the damage. Therefore, we believe that your company is implementing security policies not only because it does not trust you or your code, but also to protect itself from malicious outside code.
The principle of least authorization holds that the minimum permissions required by code should be granted in the least amount of time. That said, erect as many protective walls as possible around your code at all times. When something bad happens - as Murphy's Law guarantees - you'll be glad those protective walls are in place. Therefore, here are some specific methods for running code using the least authorization principle.
Choose a secure environment for your server code that only allows it access to the resources necessary to do its job. If some parts of your code require high permissions, consider isolating that part of the code and running it separately with higher permissions. To safely separate this code that runs with different operating system authentication information, it is best to run this code in a separate process (running in a secure environment with higher privileges). This means that you will need inter-process communication (such as COM or Microsoft .NET remoting), and you will need to design the interface to that code to minimize round-trips.
If you separate code into assemblies in a .NET Framework environment, consider the permission levels required for each piece of code. You'll find that it's an easy process: Separate the code that requires higher privileges into a separate assembly that gives it more privileges, while leaving most of the remaining assemblies running with lower privileges so that you can Add more guards around your code. When doing this, don't forget that, because of the Code Access Security (CAS) stack, you are limiting the permissions not only of your own assembly, but also of any assembly you call.
Many people build their own applications so that new components can be plugged into their products after they are tested and made available to customers. Securing this type of application is very difficult because you can't test every possible code path to find bugs and security holes. However, if your application is hosted, the CLR provides an excellent feature that can be used to close these extensibility points. By declaring a permission object or a permission set and calling PermitOnly or Deny, you add a mark to your stack that will block granting permissions to any code you call. By doing this before calling a plug-in, you can limit the tasks that the plug-in can perform. For example, a plugin for calculating installment payments does not require any access to the file system. This is just another example of least privilege, whereby you protect yourself beforehand. Be sure to note these restrictions, and be aware that higher-privileged plug-ins can use Assert statements to evade these restrictions.
8. Be aware of failure patterns
and accept them. Others hate writing error handling code as much as you do. There are so many reasons why code can fail, and it can be frustrating just to think about them. Most programmers, including us, prefer to focus on the normal execution path. That’s where the work really gets done. Let's get these error handling done as quickly and painlessly as possible, and then move on to the next line of real code.
Unfortunately, this emotion is not safe. Instead, we need to pay closer attention to the failure patterns in our code. This code is often written with little in-depth attention and is often not fully tested. Remember the last time you were absolutely sure you'd debugged every line of code in a function, including every tiny error handler in it?
Untested code often leads to security vulnerabilities. There are three things that can help you mitigate this problem. First, give those tiny error handlers the same attention as your normal code. Consider the state of the system when your error handling code executes. Is the system in an efficient and safe state? Second, once you've written a function, step through it and debug it thoroughly a few times, making sure to test every error handler. Note that even with such techniques, very subtle timing errors may not be discovered. You may need to pass an error parameter to your function, or adjust the state of the system in some way to allow your error handler to execute. By taking the time to step through your code, you can slow down and have enough time to see your code and the state of your system as it runs. By carefully stepping through the code in the debugger, we discovered many flaws in our programming logic. This is a proven technology. Please use this technique. Finally, make sure your test suite causes your function to fail. Try to have a test suite that examines every line of code in the function. This can help you spot patterns, especially when automating your tests and running them every time you build your code.
There is one very important thing to say about failure modes. Make sure your system is in the safest possible state when your code fails. Some of the problematic code is shown below:
bool accessGranted = true; // Too optimistic!
try {
// See if we can access c:test.txt
new FileStream(@"c:test.txt",
FileMode.Open,
FileAccess.Read).Close();
}
catch (SecurityException x) {
// Access denied
accessGranted = false;
}
catch (...) {
// Something else happened
}
Even though we are using the CLR, we are still allowed to access the file. In this case, a SecurityException is not thrown. But what if, for example, the file's Discretionary Access Control List (DACL) doesn't allow us access? At this time, another type of exception will be thrown. But due to the optimistic assumptions in the first line of code, we will never know this.
A better way to write this code is to be cautious:
bool accessGranted = false; // Be cautious!
try {
// See if we can access c:test.txt
new FileStream(@"c:test.txt",
FileMode.Open,
FileAccess.Read).Close();
// If we're still here, great!
accessGranted = true;
}
catch (...) {}
This will be more stable because no matter how we fail, we will always fall back to the safest mode.
9. Impersonation is highly vulnerable
When writing server applications, you will often find yourself using, directly or indirectly, a handy feature of Windows called impersonation. Impersonation allows each thread in a process to run in a different security environment, typically that of the client. For example, when a file system redirector receives a file request over the network, it authenticates the remote client, checks to confirm that the client's request does not violate the DACL on the share, and then attaches the client's flag to the thread handling the request. to simulate the client. This thread can then access the local file system on the server using the client's security environment. This is convenient since the local file system is already secure. It performs an access check taking into account the type of access requested, the DACL on the file, and the impersonation flag on the thread. If the access check fails, the local file system reports it to the file system redirector, which then sends an error to the remote client. This is undoubtedly convenient for the file system redirector, as it simply passes the request to the local file system and lets it do its own access checks, just as if the client was local.
This is all well and good for a simple gateway like a file redirector. But simulations are often used in other, more complex applications. Take a web application as an example. If you write a classic unmanaged ASP program, ISAPI extension or ASP.NET application and have the following specification in its Web.config file
<identity impersonate='true'>
then your running environment will have two different Security environment: You will have a process tag and a thread tag. Generally speaking, the thread tag will be used for access checking (see Figure 3). Assuming you are writing an ISAPI application that runs in a web server process, and assuming that most requests are unauthenticated, your thread tag might be IUSR_MACHINE, but your process tag is SYSTEM! Suppose your code can be exploited by a bad actor via a buffer overflow. Do you think it will be content with just running as IUSR_MACHINE? Of course not. His attack code most likely calls RevertToSelf to remove the impersonation flag in hopes of increasing his privilege level. In this case, he will succeed easily. He can also call CreateProcess. It does not copy the new process's tag from the impersonation tag, but from the process tag so that the new process can run as SYSTEM.
So how to solve this little problem? In addition to ensuring that no buffer overflows occur in the first place, remember the principle of least authorization. If your code does not require privileges as great as SYSTEM, do not configure your web application to run in a web server process. If you simply configure your web application to run in a medium or high isolation environment, your process tag will be IWAM_MACHINE. You don't actually have any permissions, so this attack has almost no effect. Note that in IIS 6.0 (soon to be a component of Windows .NET Server), user-written code will not run as SYSTEM by default. Based on the understanding that developers do make mistakes, any help the web server can provide in reducing the permissions given to the code is helpful in case there is a security issue in the code.
Here is another pitfall that COM programmers may encounter. COM has a bad tendency to ignore threads. If you call an in-process COM server and its thread model does not match the calling thread's model, COM performs the call on another thread. COM does not propagate the impersonation flag on the caller thread, so the result is that the call is executed in the security context of the process rather than in the security context of the calling thread. What a surprise!
Here's another example of the pitfalls of simulation. Assume that your server accepts requests sent via named pipes, DCOM, or RPC. You authenticate clients and impersonate them, opening kernel objects on their behalf through impersonation. And you forgot to close one of the objects (such as a file) when the client disconnected. When the next client comes in, you authenticate and impersonate it, and guess what happens? You can still access files that were "missed" by the previous client, even if the new client did not gain access to the file. For performance reasons, the kernel only performs access checks on an object the first time it is opened. You can still access this file even if you later change your security environment because you are impersonating another user.
The situations mentioned above are all to remind you that simulation provides convenience to server developers, but this convenience has great hidden dangers. When you run a program with a mock flag, be sure to pay careful attention to your code.
10. Write applications that non-admin users can actually use
This is really a corollary to the principle of least authorization. If programmers continue to develop code that requires an administrator to run properly on Windows, we cannot expect to improve the security of the system. Windows has a very solid set of security features, but users can't take advantage of them if they have to be an administrator to operate them.
How can you improve? First, try it yourself, without running it as administrator. You'll soon learn the pain of using a program that wasn't designed with security in mind. One day, I (Keith) installed a piece of software provided by the manufacturer of my handheld device that synchronized data between my desktop computer and my handheld device. As usual, I logged out of my normal user account, logged in again using the built-in administrator account, installed the software, then logged in again to my normal account, and tried to run the software. As a result, the application pops up a dialog box saying that a required data file cannot be accessed, and then gives an access violation message. Friends, this is the software product of a major handheld device manufacturer. Is there any excuse for this mistake?
After running FILEMON from http://sysinternals.com (in English) I quickly discovered that the application was trying to open a data file for write access which was installed in the same directory as the application's executable middle. When applications are installed in the Program Files directory as expected, they must not attempt to write data to that directory. Program Files has such a restrictive access control policy for a reason. We do not want users to write to these directories, as this would make it easy for one user to leave a Trojan for another user to execute. In fact, this convention is one of the basic signature requirements of Windos XP (see http://www.microsoft.com/winlogo [English]).
We hear too many programmers give excuses as to why they choose to run as administrator when developing code. If we continue to ignore this problem, we will only make matters worse. Friends, you do not need administrator rights to edit a text file. Administrator rights are also not required to edit or debug a program. When you need administrator privileges, use the operating system's RunAs feature to run 玎com.asp?TARGET=/winlogo/">http://www.microsoft.com/winlogo [English]).
We hear this too much Programmers give excuses why they choose to run as administrator when developing code. If we continue to ignore this problem, it will only make things worse. Editing a text file does not require administrator privileges. Or debugging a program does not require administrator privileges. When you need administrator privileges, use the operating system's RunAs feature to run a separate program with elevated privileges (see the November 2001 Security Briefs [English]. column). If you are writing tools for developers, you have an additional responsibility for this group. We need to stop this vicious cycle of writing code that can only be run as an administrator. The goal has to change fundamentally.
For more information on how developers can easily run as non-administrators, see Keith's Web site at http://www.develop.com/kbrown (in English). Check out Michael's book Writing Secure Code (Microsoft Press, 2001), which provides tips on how to write applications that run well in a non-administrator environment.