Tuesday, June 8, 2021

Setting samesite:strict in pre .Net 4.7.2 versions to fix CSRF

CSRF is a type of web security attack where the attacker site loaded side by side with our web application and attacker site making HTTP requests to our application as the signed-in user. This happens when our authentication mechanism is based on cookies and is accessible to the attacker's site. When the attacker site postback or make HTTP GET call, the cookie is transmitted to our web application and it identifies as a valid user.

How to fix it?

There are 2 ways to my understanding.

Traditional (Pre SPA - Single Page Application)

This method relies upon one more security measure on the top of the authentication cookie. That is normally called as RequestVerificationToken. Below is the working 

  1. When the page is served to the client, the server will inject an encrypted token in a hidden field
  2. The client fills data in the form. Then perform submit operation.
  3. The incoming POST request is validated by the server for the hidden field. If that field is not available or not able to decrypt using the key in hand, reject the request.
Now think about the attacker's site posting back, that request has the cookie but not the hidden field. This is because the attacker site cannot access the DOM of our web application.

SPA

SPA model doesn't use page-level postback. Instead, it sends and receives data in AJAX requests. The web application's static assets (HTML, JS & CSS) normally loaded during the first time of application load. It may even be served from a CDN. We cannot use the RequestVerificationToken via the hidden field here as the page is not posted back when we do operations.

Here we have another way. It is nothing but fixing the original problem. Blocking the browser not to send the auth cookie when a request is made from another web page, usually the attacker's site. In order to tell the browser not to share cookies, we can use the Same-Site flag on the authentication cookie. This should be done by the server at the time of initial login. The value should be set to same-site: strict.

Now we are going to see how to set the same-site flag to strict in ASP.Net. 

Setting Same-Site to Strict in ASP.Net

If we are using the latest version of .Net starting from 4.7.2, it is easy as below code snippet.

 HttpCookie sameSiteCookie = new HttpCookie("AuthCookie");
 sameSiteCookie.SameSite = SameSiteMode.Strict;

Another method is to use the web.config without any code change. We even can intercept cookies that are written by other components of the system,

Prior to .Net 4.7.2

As per Microsoft, they don't support earlier .Net versions such as .Net 4.5, .Net 4.5.1, .Net 4.5.2, .Net 4.6, .Net 4.6.1, .Net 4.6.2, .Net 4.7.x, etc.. also there is no reliable way to get it done. Keep the reliability aside, they didn't even provide any method to set it.


I agree giving an unreliable way doesn't make sense. But I am just attempting one way as given below.

private void AddCSRFCookieIfNotPresent(object source)
{
    HttpApplication application = (HttpApplication)source;
    HttpContext context = application.Context;
    if (!isCSRFTokenHttpCookieAvalible(context.Request))
    {
        string cookieSetBySomeone = context.Response.Headers["Set-Cookie"];
        if (string.IsNullOrWhiteSpace(cookieSetBySomeone))
        {
            context.Response.AddHeader("Set-Cookie", 
                "SameSiteSample=sample; expires=Thu, 28-May-2021 22:08:36 GMT; path=/; secure; HttpOnly;SameSite=Strict");
        }
        else
        {
            // Caution - If someone had set the cookie, it may be overwritten.
            // Else extract existing cookie header, adjust it to include SameSiteSample. Then put it back.
            Trace.WriteLine(cookieSetBySomeone);
        }
    }
}

Hope this explains. Please read the caution part. It uses direct Set-Cookie header adjustment.

Now the question is where this function should be written? How to connect to the ASP.Net request processing pipeline?

The answer is kind of depends. But the simple way is to just wrap in HttpModule. The full code goes below.

 public class CSRFTokenHttpModule : IHttpModule
    {
        private void Application_BeginRequest(Object source, EventArgs e)
        {
            AddCSRFCookieIfNotPresent(source);
        }

        private void AddCSRFCookieIfNotPresent(object source)
        {
            HttpApplication application = (HttpApplication)source;
            HttpContext context = application.Context;
            if (!isCSRFTokenHttpCookieAvalible(context.Request))
            {
                string cookieSetBySomeone = context.Response.Headers["Set-Cookie"];
                if (string.IsNullOrWhiteSpace(cookieSetBySomeone))
                {
                    context.Response.AddHeader("Set-Cookie", 
                        "SameSiteSample=sample; expires=Thu, 28-May-2021 22:08:36 GMT; path=/; secure; HttpOnly;SameSite=Strict");
                }
                else
                {
                    // Caution - If someone had set the cookie, it may be overwritten.
                    // Else extract existing cookie header, adjust it to include SameSiteSample. Then put it back.
                    Trace.WriteLine(cookieSetBySomeone);
                }
            }
        }

        private bool isCSRFTokenHttpCookieAvalible(HttpRequest Request)
        {
            for (var i = 0; i < Request.Cookies.Count; i++)
            {
                var cookie = Request.Cookies[i];
                if (cookie.Name == "SameSiteSample")
                {
                    return true;
                }
            }
            return false;
        }
        public void Dispose()
        {
            // nothing to do.
        }

        public void Init(HttpApplication context)
        {
            context.BeginRequest +=
                (new EventHandler(this.Application_BeginRequest));
        }
    }

Below goes the web.config entries to connect the class to the ASP.Net pipeline.

    <modules runAllManagedModulesForAllRequests="true">
      <clear/>
      <add name="CSRFTokenHttpModule" type="CSRFHttpModule.CSRFTokenHttpModule,CSRFHttpModule" preCondition="" />
    </modules>

This is just an attempt out of curiosity, not production-ready. Please try at your risk.

The recommendation is to upgrade the .Net framework and use the supported way.

Rewrite

There are some sites and SO posts talking about rewriting the cookie that adds the same-site flag. This is not tried. Just added it in case it helps someone.

https://www.componentspace.com/Forums/10843/Ramifications-of-setting-httpCookies-sameSite-in-webconfig

https://stackoverflow.com/questions/59117357/how-samesite-attribute-added-to-my-asp-net-sessionid-cookie-automatically/60357945#60357945

No comments: