The Basics of Securing Applications: Vulnerabilities
In this post, we’re continuing our One on One with Visual Studio conversation from May 26 with Canadian Developer Security MVP Steve Syfuhs, The Basics of Securing Applications.If you’ve just joined us, the conversation builds on the previous post, so check that out (links below) and then join us back here. If you’ve already read the previous post, welcome!
Part 1: Development Security Basics
Part 2: Vulnerability Deep Dive (This Post)
In this session of our conversation, The Basics of Securing Applications, Steve goes through the primers of writing secure code, focusing specifically on the most common threats to web-based applications.
Steve, back to you.
Great, thank you. When we started the conversation last week, I stated that knowledge is key to writing secure code:
Perhaps the most important aspect of the SDL is that it's important to have a good foundation of knowledge of security vulnerabilities.
In order to truly protect our applications and the data within, we need to know about the threats in the real world. The OWASP top 10 list gives a us a good starting point in understanding some of the more common vulnerabilities that malicious users can use for attack:
- Injection
- Cross-Site Scripting (XSS)
- Broken Authentication and Session Management
- Insecure Direct Object References
- Cross-Site Request Forgery (CSRF)
- Security Misconfiguration
- Insecure Cryptographic Storage
- Failure to Restrict URL Access
- Insufficient Transport Layer Protection
- Unvalidated Redirects and Forwards
It's important to realize that is strictly for the web – we aren't talking client applications, though some do run into the same problems. We’ll focus on web-based applications for now. It's also helpful to keep in mind that we aren't talking just Microsoft-centric vulnerabilities. These also exist in applications running on the LAMP (Linux/Apache/MySQL/PHP) stack and variants in between. Finally, it's very important to note that just following these instructions won't automatically give you a secure code base – these are just primers in some ways of writing secure code.
Injection
Injection is a way of changing the flow of a procedure by introducing arbitrary changes. An example is SQL injection. Hopefully you’ve heard of SQL Injection, but lets take a look at this bit of code for those who don't know about it:
string query = string.Format("SELECT * FROM UserStore WHERE UserName = '{0}' AND PasswordHash = '{1}'", username, password);
If we passed it into a SqlCommand we could use it to see whether or not a user exists, and whether or not their hashed password matches the one in the table. If so, they are authenticated. Well what happens if I enter something other than my username? What if I enter this:
'; --
It would modify the SQL Query to be this:
SELECT * FROM UserStore WHERE UserName = ''; -- AND PasswordHash = 'TXlQYXNzd29yZA=='
This has essentially broken the query into a single WHERE clause, asking for a user with a blank username because the single quote closed the parameter, the semicolon finished the executing statement, and the double dash made anything following it into a comment.
Hopefully your user table doesn't contain any blank records, so lets extend that a bit:
' OR 1=1; --
We've now appended a new clause, so the query looks for records with a blank username OR where 1=1. Since 1 always equals 1, it will return true, and since the query looks for any filter that returns true, it returns every record in the table.
If our SqlCommand just looked for at least one record in the query set, the user is authenticated. Needless to say, this is bad.
We could go one step further and log in as a specific user:
administrator'; --
We've now modified the query in such a way that it is just looking for a user with a particular username, such as the administrator. It only took four characters to bypass a password and log in as the administrator.
Injection can also work in a number of other places such as when you are querying Active Directory or WMI. It doesn't just have to be for authentication either. Imagine if you have a basic report query that returns a large query set. If the attacker can manipulate the query, they could read data they shouldn't be allowed to read, or worse yet they could modify or delete the data.
Essentially our problem is that we don't sanitize our inputs. If a user is allowed to enter any value they want into the system, they could potentially cause unexpected things to occur. The solution is simple: sanitize the inputs!
If we use a SqlCommand object to execute our query above, we can use parameters. We can write something like:
string query = "SELECT * FROM UserStore WHERE UserName = @username AND PasswordHash = @passwordHash"; SqlCommand c = new SqlCommand(query); c.Parameters.Add(new SqlParameter("@username", username)); c.Parameters.Add(new SqlParameter("@passwordHash", passwordHash));
This does two things. One, it makes .NET handle the string manipulation, and two it makes .NET properly sanitize the parameters, so
' OR 1=1; --
is converted to
' '' OR 1=1;—'
In the SQL language, two single quote characters acts as an escape sequence for a single quote, so in effect the query is trying to look for a value as is, containing the quote.
The other option is to use a commercially available Object Relational Mapper (ORM) like the Entity Framework or NHibernate where you don't have to write error-prone SQL queries. You could write something like this with LINQ:
var users = from u in entities.UserStore
where u.UserName == username && u.PasswordHash == passwordHash select u;
It looks like a SQL query, but it's compileable C#. It solves our problem by abstracting away the ungodly mess that is SqlCommands, DataReaders, and DataSets.
Cross-Site Scripting (XSS)
XSS is a way of adding a chunk of malicious JavaScript into a page via flaws in the website. This JavaScript could do a number of different things such as read the contents of your session cookie and send it off to a rogue server. The person in control of the server can then use the cookie and browse the affected site with your session. In 2007, 80% of the reported vulnerabilities on the web were from XSS.
XSS is generally the result of not properly validating user input. Conceptually it usually works this way:
- A query string parameter contains a value: ?q=blah
- This value is outputted on the page
- A malicious user notices this
- The malicious user inserts a chunk of JavaScript into the URL parameter: ?q=<script>alert("pwned");</script>
- This script is outputted without change to the page, and the JavaScript is executed
- The malicious user sends the URL to an unsuspecting user
- The user clicks on it while logged into the website
- The JavaScript reads the cookie and sends it to the malicious user's server
- The malicious user now has the unsuspecting user's authenticated session
This occurs because we don't sanitize user input. We don't remove or encode the script so it can't execute. We therefore need to encode the inputted data. It's all about the sanitized inputs.
The basic problem is that we want to display the content the user submitted on a page, but the content can be potentially executable. Well, how do we display JavaScript textually? We encode each character with HtmlEncode, so the < (left angle bracket) is outputted as < and the > (right angle bracket) is outputted as >. In .NET you have have some helpers in the HttpUtility class:
HttpUtility.HtmlEncode("<script>alert(\"hello!\");</script>");
This works fairly well, but you can bypass it by doing multiple layers of encoding (encoding an encoded value that was encoded with another formula). This problem exists because HtmlEncode uses a blacklist of characters, so whenever it comes across a specific character it will encode it. We want it to do the opposite – use a whitelist. So whenever it comes across a known character, it doesn't encode it (such as the letter 'a'), but otherwise it encodes everything else. It's generally far easier to protect something if you only allow known good things instead of blocking known threats (because threats are constantly changing).
Microsoft released a toolkit to solve this encoding problem, called the AntiXss toolkit. It's now part of the Microsoft Web Protection Library, which also actually contains some bits to help solve the SQL injection problem. To use this encoder, you just need to do something like this:
string encodedValue = Microsoft.Security.Application.Sanitizer.GetSafeHtmlFragment(userInput);
There is another step, which is to set the cookie to server-only, meaning that client side scripts cannot read the contents of the cookie. Only newer browsers support this, but all we have to do is write something like this:
HttpCookie cookie = new HttpCookie("name", "value"); cookie.HttpOnly = true;
For added benefit while we are dealing with cookies, we can also do this:
cookie.Secure = true;
Setting Secure to true requires that the cookie only be sent over HTTPS.
This should be the last step in the output. There shouldn't be any tweaking to the text or cookie after this point. Call it the last line of defense on the server-side.
Cross-Site Request Forgery (CSRF)
Imagine a web form that has a couple fields on it – sensitive fields, say money transfer fields: account to, amount, transaction date, etc. You need to log in, fill in the details, and click submit. That submit POSTs the data back to the server, and the server processes it. In ASP.NET WebForms, the only validation that goes on is whether the ViewState hasn’t been tampered with. Other web frameworks skip the ViewState bit, because well, they don't have a ViewState.
Now consider that you are still logged in to that site, and someone sends you a link to a funny picture of a cat. Yay, kittehs! Anyway, on that page is a simple set of hidden form tags with malicious data in it. Something like their account number, and an obscene number for cash transfer. On page load, JavaScript POST’s that form data to the transfer page, and since you are already logged in, the server accepts it. Sneaky.
There is actually a pretty elegant way of solving this problem. We need to create a value that changes on every page request, and send it as part of the response. Once the server receives the response, it validates the value and if it's bad it throws an exception. In the the cryptography world, this is called a nonce . In ASP.NET WebForms we can solve this problem by encrypting the ViewState. We just need a bit of code like this in the page (or masterpage):
void Page_Init (object sender, EventArgs e) { ViewStateUserKey = Session.SessionID; }
When we set the ViewStateUserKey property on the page, the ViewState is encrypted based on this key. This key is only valid for the length of the session, so this does two things. First, since the ViewState is encrypted, the malicious user cannot modify their version of the ViewState since they don't know the key. Second, if they use an unmodified version of a ViewState, the server will throw an exception since the victim's UserKey doesn't match the key used to encrypt the initial ViewState, and the ViewState parser doesn't understand the value that was decrypted with the wrong key. Using this piece of code depends entirely on whether or not you have properly set up session state though. To get around that, we need to set the key to a cryptographically random value that is only valid for the length of the session, and is only known on the server side. We could for instance use the modifications we made to the cookie in the XSS section, and store the key in there. It gets passed to the client, but client script can't access it. This places a VERY high risk on the user though, because this security depends entirely on the browser version. It also means that any malware installed on the client can potentially read the cookie – though the user has bigger problems if they have a virus.
Security is complex, huh? Anyway…
In MVC we can do something similar except we use the Html.AntiForgeryToken().
This is a two step process. First we need to update the Action method(s) by adding the ValidateAntiForgeryToken attribute to the method:
[AcceptVerbs(HttpVerbs.Post)] [ValidateAntiForgeryToken] public ActionResult Transfer(WireTransfer transfer) { try { if (!ModelState.IsValid) return View(transfer); context.WireTransfers.Add(transfer); context.SubmitChanges(); return RedirectToAction("Transfers"); } catch { return View(transfer); } }
Then we need to add the AntiForgeryToken to the page:
<%= Html.AntiForgeryToken() %>
This helper will output a nonce that gets checked by the ValidateAntiForgeryToken attribute.
Insecure Cryptographic Storage
I think it's safe to say that most of us get cryptography related-stuff wrong most of the time at first. I certainly do. Mainly because crypto is hard to do properly. If you noticed above in my SQL Injection query, I used this value for my hashed password: TXlQYXNzd29yZA==.
It's not actually hashed. It's encoded using Base64 (the double-equals is a dead give away). The decoded value is 'MyPassword'. The difference being that hashing is a one-way process. Once I hash something (with a cryptographic hash), I can't de-hash it. Second, if I happen to get hold of someone else's user table, I can look for hashed passwords that look the same. So anyone else that has a password in the table as "TXlQYXNzd29yZA==", I know that their password is 'MyPassword'. This is where a salt comes in handy. A salt is just a chunk of data appended to the unhashed password, and then hashed. Each user has a unique salt, and therefore will have a unique hash.
Then in the last section on CSRF I talked about using a nonce. Nonce's are valuable to the authenticity of a request. They prevent replay attacks, meaning that the encrypted output will look the same as a previous response, and is therefore a copy of the last message. It is extremely important that the attacker not know how this nonce is generated.
Which leads to the question of how to do you properly secure encryption keys? Well that’s a discussion in and of itself. Properly using cryptography in an application is really hard to do. Proper cryptography in an application is a topic fit for a book.
Final Thoughts
We’ve touched on only four of the items in the OWASP top 10 list as they are directly solvable using publically available frameworks. The other six items in the list can be solved through the use of tools as well as designing a secure architecture, both of which we will talk about in the future.
Looking forward to continuing the conversation.
About Steve Syfuhs
Steve Syfuhs is a bit of a Renaissance Kid when it comes to technology. Part developer, part IT Pro, part Consultant working for ObjectSharp. Steve spends most of his time in the security stack with special interests in Identity and Federation. He recently received a Microsoft MVP award in Developer Security. You can find his ramblings about security at www.steveonsecurity.com |