15

We're experiencing a weird problem with the payment process of our web application which results in loss of session data.

In this process, after our check-out page user is redirected to payment provider's page and redirected back to our site (to a url we specify) as soon as s/he's done there. This last redirect is done by browser's evaluation of the payment provider's html code which basically consists of a form that posts to our site and a few lines of javascript code that posts that form on page load. At this point browser makes the post request but does not set the "ASP.NET_SessionId" cookie which is present in the previous requests made to the exact same domain (our application's domain). What's more weird is that it sets another cookie we use named "AcceptCookie". It just simply chooses to drop "ASP.NET_SessionId" cookie.

To illustrate the situation I took some screenshots. (In these screenshots orange and green rectangles contain exactly the same value.)

  1. This is the request that made (to our application) when user presses "Check Out" button. After this request user is redirected to payment provider's page.

check-out request

  1. This is the final page that is served by payment provider after user is done there. As you can see it's just a simple form that is automatically posted to our domain on page load.

payment provider's final response

  1. But this post request does not include "ASP.NET_SessionId" cookie which results in acquiring a new session id and the loss of previous session data. And again, just "ASP.NET_SessionId" is missing, not the other one named "AcceptCookie".

post request that brings the user back to our site (made with javascript in the previous step)

Finally we figured that on the older versions of browsers this problem does not occur. On Firefox 52 it works like a charm but on Firefox 71 the above problem happens.

Any ideas?

Note: It's an ASP.NET MVC application with targetFramework="4.5.2"

Have a nice day.

Daniel Widdis
  • 8,424
  • 13
  • 41
  • 63
E. Özgür
  • 361
  • 2
  • 8

3 Answers3

21

We figured it out.

Somehow "ASP.NET_SessionId" cookie's "SameSite" attribute defaults to "Lax" and this causes session cookie not being added to the request that made by payment gateway's javascript code.

We added following rule to the web.config file in order to override this value and set it to "None".

<configuration>
  <system.webServer>
    <rewrite>
      <outboundRules>
        <rule name="Add SameSite" preCondition="No SameSite">
          <match serverVariable="RESPONSE_Set_Cookie" pattern=".*" negate="false" />
          <action type="Rewrite" value="{R:0}; SameSite=None" />
          <conditions>
          </conditions>
        </rule>
        <preConditions>
          <preCondition name="No SameSite">
            <add input="{RESPONSE_Set_Cookie}" pattern="." />
            <add input="{RESPONSE_Set_Cookie}" pattern="; SameSite=None" negate="true" />
          </preCondition>
        </preConditions>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>

UPDATE 1: Just adding above configuration solved the problem for modern browsers but we realized that we were still having issues with older versions of Micosoft Edge and Internet Explorer.

So we needed to add cookieSameSite="None" attribute to sessionState node in web.config file.

<sessionState cookieSameSite="None" />

Be careful with this configuration change though, as older .net framework versions do not support it and cause your site to display error page.

By the way we're still having issues with browsers in IOS 12. But I think it's related to this confirmed bug

UPDATE 2: see zemien's answer for possible fix about IOS issue

UPDATE 3: By combining our findings with the suggestions in zemien's answer we've come up with the following rewrite rules. We've been using this configuration in production. But beware: it marks all the cookies with "SameSite:None" attribute for compatible browsers and excludes SameSite attribute, if exists, for incompatible browsers. It may seem complicated but I tried to explain via comment lines.

This is the FINAL configuration we use in production:

<configuration> 

  <system.webServer>

    <rewrite>

      <outboundRules>

        <preConditions>
          <!-- Browsers incompatible with SameSite=None -->
          <preCondition name="IncompatibleWithSameSiteNone" logicalGrouping="MatchAny">
            <add input="{HTTP_USER_AGENT}" pattern="(CPU iPhone OS 12)|(iPad; CPU OS 12)" />
            <add input="{HTTP_USER_AGENT}" pattern="(Chrome/5)|(Chrome/6)" />
            <add input="{HTTP_USER_AGENT}" pattern="( OS X 10_14).*(Version/).*((Safari)|(KHTML, like Gecko)$)" />
          </preCondition>

          <!-- Rest of the browsers are assumed to be compatible with SameSite=None -->
          <preCondition name="CompatibleWithSameSiteNone" logicalGrouping="MatchAll">
            <add input="{HTTP_USER_AGENT}" pattern="(CPU iPhone OS 12)|(iPad; CPU OS 12)" negate="true" />
            <add input="{HTTP_USER_AGENT}" pattern="(Chrome/5)|(Chrome/6)" negate="true" />
            <add input="{HTTP_USER_AGENT}" pattern="( OS X 10_14).*(Version/).*((Safari)|(KHTML, like Gecko)$)" negate="true" />
          </preCondition>

        </preConditions>

        <!-- Rule 1: Remove SameSite part from cookie for incompatible browsers if exists -->
        <rule name="Remove_SameSiteCookie_IfExists_ForLegacyBrowsers" preCondition="IncompatibleWithSameSiteNone">
          <match serverVariable="RESPONSE_Set-Cookie" pattern="(.*)(SameSite=.*)" />
          <action type="Rewrite" value="{R:1}" />
        </rule>

        <!-- Rule 2: Override SameSite's value to None if exists, for compatible browsers -->
        <rule name="Override_SameSiteCookie_IfExists_ForModernBrowsers" preCondition="CompatibleWithSameSiteNone">
          <match serverVariable="RESPONSE_Set-Cookie" pattern="(.*)(SameSite=.*)" />
          <action type="Rewrite" value="{R:1}; SameSite=None" />
        </rule>

        <!-- Rule 3: Add SameSite attribute with the value None if it does not exists, for compatible browsers -->
        <rule name="Add_SameSiteCookie_IfNotExists_ForModernBrowsers" preCondition="CompatibleWithSameSiteNone">
          <match serverVariable="RESPONSE_Set-Cookie" pattern=".*"/>
          <!-- Condition explanation: Cookie data contains some string value but does not contain SameSite attribute -->
          <conditions logicalGrouping="MatchAll">
            <add input="{R:0}" pattern="^(?!\s*$).+"/>
            <add input="{R:0}" pattern="SameSite=.*" negate="true"/>
          </conditions>
          <action type="Rewrite" value="{R:0}; SameSite=None" />
        </rule>

      </outboundRules>

    </rewrite>    

  </system.webServer>  

</configuration>
E. Özgür
  • 361
  • 2
  • 8
  • Thanks @EÖzgür. This problem come from KB4533097 (https://support.microsoft.com/en-us/help/4533097/kb4533097) on most specifically KB4533011 (.net 4.7 and lower) and KB4533004 (.net 4.8) released on december 10. – S. Pineau Jan 16 '20 at 14:21
  • I have the same problem, but sometimes asp.net mvc give the customer ASP.NET_SessionId cookies with LAX sometimes with NONE. I am not sure why it happends. I mean it should be LAX all the time, but still when I login on site I can get NONE. – Duke Jan 23 '20 at 18:06
  • Oh man! I have been crazy about this issue for two days. Finally your answer saved my day and frustration. Thanks. – Hemanth Jan 25 '20 at 10:53
  • 1
    We had this problem happen on Server 2016 after applying December updates. (KB4530689). Many thanks for finding the solution! – user0474975 Jan 28 '20 at 14:04
  • Is this only for dotnet core? In my Framework application I'm showing those your options as invalid values to set. – IronSean Jan 28 '20 at 21:15
  • @IronSean these configurations are for .net framework only. – E. Özgür Jan 30 '20 at 10:09
  • You can fix IOS 12 problem by replacing following in your answer: Replace with – Mohammad Reza Sadreddini Jan 30 '20 at 11:26
  • Thank you and zemien (below) for the detailed answers. Your information was extremely helpful. – Puzzled Apr 12 '20 at 01:13
3

I modified upon several SO answers to come up with this URL rewrite that adds SameSite=None to session cookies, and also remove SameSite=None from all cookies for most incompatible browsers. The aim of this rewrite is to preserve the "legacy" behaviour pre-Chrome 80.

Full write-up in my Coder Frontline blog:

<rewrite>
  <outboundRules>
    <preConditions>
      <!-- Checks User Agent to identify browsers incompatible with SameSite=None -->
      <preCondition name="IncompatibleWithSameSiteNone" logicalGrouping="MatchAny">
        <add input="{HTTP_USER_AGENT}" pattern="(CPU iPhone OS 12)|(iPad; CPU OS 12)" />
        <add input="{HTTP_USER_AGENT}" pattern="(Chrome/5)|(Chrome/6)" />
        <add input="{HTTP_USER_AGENT}" pattern="( OS X 10_14).*(Version/).*((Safari)|(KHTML, like Gecko)$)" />
      </preCondition>
    </preConditions>

    <!-- Adds or changes SameSite to None for the session cookie -->
    <!-- Note that secure header is also required by Chrome and should not be added here -->
    <rule name="SessionCookieAddNoneHeader">
      <match serverVariable="RESPONSE_Set-Cookie" pattern="((.*)(ASP.NET_SessionId)(=.*))(SameSite=.*)?" />
      <action type="Rewrite" value="{R:1}; SameSite=None" />
    </rule>

    <!-- Removes SameSite=None header from all cookies, for most incompatible browsers -->
    <rule name="CookieRemoveSameSiteNone" preCondition="IncompatibleWithSameSiteNone">
      <match serverVariable="RESPONSE_Set-Cookie" pattern="(.*)(SameSite=None)" />
      <action type="Rewrite" value="{R:1}" />
    </rule>
  </outboundRules>
</rewrite>

This should work for most ASP .Net and ASP .Net Core applications, although newer Frameworks have proper code and config options to let you control this behaviour. I would recommend researching all the options available to you before using my rewrite above.

zemien
  • 562
  • 6
  • 17
0

If you don't want to deploy secure, SameSite=None cookies then another option is to simply have the browser set the cookie in JavaScript on page load. This approach works for all browsers without requiring any special cases for various browsers. In an ASP.NET MVC application, it can be accomplished by simply adding the following to the layout.cshtml page:

<script type="text/javascript">
    // If being rendered in an iFrame, set a client-side cookie for the ASP.NET Session ID
    if (window != window.top) {
        document.cookie = "ASP.NET_SessionID=@HttpContext.Current.Session.SessionID";
    }
</script>

This is effectively passing the cookie value to the client through the HTML payload, and the client then overwrites the ASP.NET_SessionID cookie that may or may not have been accepted by the browser. Once the cookie is set, then any request that is made will pass the cookie back to the server. Note that this approach does not let you specify HttpOnly for the session cookie.

I wouldn't recommend this approach for public facing websites, but for intranet applications this is a pretty quick workaround.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77