Skip to content

Commit 60da3b3

Browse files
Bind CSRF token to session id for .NET. (#872)
* Bind CSRF token to session id for .NET. Move VerificationToken service to root path (rest/) * New sessions must not validate session data bound to CSRF token. * Remove session binding to the CSRF token, as it is unnecessary for serverless services on the backend.
1 parent 4959fe1 commit 60da3b3

File tree

7 files changed

+134
-113
lines changed

7 files changed

+134
-113
lines changed
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
2+
3+
using System;
4+
using System.Threading.Tasks;
5+
using GeneXus.Configuration;
6+
using GeneXus.Http;
7+
using log4net;
8+
using Microsoft.AspNetCore.Antiforgery;
9+
using Microsoft.AspNetCore.Http;
10+
namespace GeneXus.Application
11+
{
12+
public class ValidateAntiForgeryTokenMiddleware
13+
{
14+
static readonly ILog log = log4net.LogManager.GetLogger(typeof(ValidateAntiForgeryTokenMiddleware));
15+
16+
private readonly RequestDelegate _next;
17+
private readonly IAntiforgery _antiforgery;
18+
private string _basePath;
19+
20+
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery, String basePath)
21+
{
22+
_next = next;
23+
_antiforgery = antiforgery;
24+
_basePath = "/" + basePath;
25+
}
26+
27+
public async Task Invoke(HttpContext context)
28+
{
29+
if (context.Request.Path.HasValue && context.Request.Path.Value.StartsWith(_basePath))
30+
{
31+
if (HttpMethods.IsPost(context.Request.Method) ||
32+
HttpMethods.IsDelete(context.Request.Method) ||
33+
HttpMethods.IsPut(context.Request.Method))
34+
{
35+
string cookieToken = context.Request.Cookies[HttpHeader.X_CSRF_TOKEN_COOKIE];
36+
string headerToken = context.Request.Headers[HttpHeader.X_CSRF_TOKEN_HEADER];
37+
GXLogging.Debug(log, $"Antiforgery validation, cookieToken:{cookieToken}, headerToken:{headerToken}");
38+
39+
await _antiforgery.ValidateRequestAsync(context);
40+
GXLogging.Debug(log, $"Antiforgery validation OK");
41+
}
42+
else if (HttpMethods.IsGet(context.Request.Method))
43+
{
44+
SetAntiForgeryTokens(_antiforgery, context);
45+
}
46+
}
47+
if (!context.Request.Path.Value.EndsWith(_basePath)) //VerificationToken
48+
await _next(context);
49+
}
50+
internal static void SetAntiForgeryTokens(IAntiforgery _antiforgery, HttpContext context)
51+
{
52+
AntiforgeryTokenSet tokenSet = _antiforgery.GetAndStoreTokens(context);
53+
string sameSite;
54+
CookieOptions cookieOptions = new CookieOptions { HttpOnly = false, Secure = GxContext.GetHttpSecure(context) == 1 };
55+
SameSiteMode sameSiteMode = SameSiteMode.Unspecified;
56+
if (Config.GetValueOf("SAMESITE_COOKIE", out sameSite) && Enum.TryParse(sameSite, out sameSiteMode))
57+
{
58+
cookieOptions.SameSite = sameSiteMode;
59+
}
60+
context.Response.Cookies.Append(HttpHeader.X_CSRF_TOKEN_COOKIE, tokenSet.RequestToken, cookieOptions);
61+
GXLogging.Debug(log, $"Setting cookie ", HttpHeader.X_CSRF_TOKEN_COOKIE, "=", tokenSet.RequestToken, " samesite:" + sameSiteMode);
62+
}
63+
64+
}
65+
}

dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs

Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
2-
31
using System;
42
using System.Collections.Generic;
53
using System.IO;
64
using System.Net;
7-
using System.Runtime.InteropServices;
85
using System.Threading.Tasks;
96
using Azure.Identity;
107
using Azure.Monitor.OpenTelemetry.Exporter;
@@ -253,7 +250,7 @@ public void ConfigureServices(IServiceCollection services)
253250
{
254251
services.AddAntiforgery(options =>
255252
{
256-
options.HeaderName = HttpHeader.X_GXCSRF_TOKEN;
253+
options.HeaderName = HttpHeader.X_CSRF_TOKEN_HEADER;
257254
options.SuppressXFrameOptionsHeader = false;
258255
});
259256
}
@@ -460,20 +457,10 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
460457
routes.MapRoute($"{s}", new RequestDelegate(gxRouting.ProcessRestRequest));
461458
}
462459
}
463-
routes.MapRoute($"{restBasePath}VerificationToken", (context) =>
464-
{
465-
string requestPath = context.Request.Path.Value;
466-
467-
if (string.Equals(requestPath, $"/{restBasePath}VerificationToken", StringComparison.OrdinalIgnoreCase) && antiforgery!=null)
468-
{
469-
ValidateAntiForgeryTokenMiddleware.SetAntiForgeryTokens(antiforgery, context);
470-
}
471-
return Task.CompletedTask;
472-
});
473460
routes.MapRoute($"{restBasePath}{{*{UrlTemplateControllerWithParms}}}", new RequestDelegate(gxRouting.ProcessRestRequest));
474461
routes.MapRoute("Default", VirtualPath, new { controller = "Home", action = "Index" });
475462
});
476-
463+
477464
app.UseWebSockets();
478465
string basePath = string.IsNullOrEmpty(VirtualPath) ? string.Empty : $"/{VirtualPath}";
479466
Config.ScriptPath = basePath;
@@ -630,60 +617,4 @@ public IActionResult Index()
630617
return Redirect(defaultFiles[0]);
631618
}
632619
}
633-
public class ValidateAntiForgeryTokenMiddleware
634-
{
635-
static readonly ILog log = log4net.LogManager.GetLogger(typeof(ValidateAntiForgeryTokenMiddleware));
636-
637-
private readonly RequestDelegate _next;
638-
private readonly IAntiforgery _antiforgery;
639-
private string _basePath;
640-
641-
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery, String basePath)
642-
{
643-
_next = next;
644-
_antiforgery = antiforgery;
645-
_basePath = "/" + basePath;
646-
}
647-
648-
public async Task Invoke(HttpContext context)
649-
{
650-
if (context.Request.Path.HasValue && context.Request.Path.Value.StartsWith(_basePath))
651-
{
652-
if (HttpMethods.IsPost(context.Request.Method) ||
653-
HttpMethods.IsDelete(context.Request.Method) ||
654-
HttpMethods.IsPut(context.Request.Method))
655-
{
656-
string cookieToken = context.Request.Cookies[HttpHeader.X_GXCSRF_TOKEN];
657-
string headerToken = context.Request.Headers[HttpHeader.X_GXCSRF_TOKEN];
658-
GXLogging.Debug(log, $"Antiforgery validation, cookieToken:{cookieToken}, headerToken:{headerToken}");
659-
660-
await _antiforgery.ValidateRequestAsync(context);
661-
GXLogging.Debug(log, $"Antiforgery validation OK");
662-
}
663-
else if (HttpMethods.IsGet(context.Request.Method))
664-
{
665-
string tokens = context.Request.Cookies[HttpHeader.X_GXCSRF_TOKEN];
666-
if (string.IsNullOrEmpty(tokens))
667-
{
668-
SetAntiForgeryTokens(_antiforgery, context);
669-
}
670-
}
671-
}
672-
await _next(context);
673-
}
674-
internal static void SetAntiForgeryTokens(IAntiforgery _antiforgery, HttpContext context)
675-
{
676-
AntiforgeryTokenSet tokenSet = _antiforgery.GetAndStoreTokens(context);
677-
string sameSite;
678-
CookieOptions cookieOptions = new CookieOptions { HttpOnly = false, Secure = GxContext.GetHttpSecure(context) == 1 };
679-
SameSiteMode sameSiteMode= SameSiteMode.Unspecified;
680-
if (Config.GetValueOf("SAMESITE_COOKIE", out sameSite) && Enum.TryParse(sameSite, out sameSiteMode))
681-
{
682-
cookieOptions.SameSite = sameSiteMode;
683-
}
684-
context.Response.Cookies.Append(HttpHeader.X_GXCSRF_TOKEN, tokenSet.RequestToken, cookieOptions);
685-
GXLogging.Debug(log, $"Setting cookie ", HttpHeader.X_GXCSRF_TOKEN, "=", tokenSet.RequestToken, " samesite:" + sameSiteMode);
686-
}
687-
688-
}
689620
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Net.Http;
2+
using System.Security;
3+
using System.Web;
4+
using System.Web.Helpers;
5+
using GeneXus.Application;
6+
using GeneXus.Utils;
7+
8+
namespace GeneXus.Http
9+
{
10+
internal class CSRFHelper
11+
{
12+
[SecuritySafeCritical]
13+
internal static void ValidateAntiforgery(HttpContext context)
14+
{
15+
if (RestAPIHelpers.ValidateCsrfToken())
16+
{
17+
ValidateAntiforgeryImpl(context);
18+
}
19+
}
20+
[SecurityCritical]
21+
static void ValidateAntiforgeryImpl(HttpContext context)
22+
{
23+
string cookieToken, formToken;
24+
string httpMethod = context.Request.HttpMethod;
25+
string tokens = context.Request.Cookies[HttpHeader.X_CSRF_TOKEN_COOKIE]?.Value;
26+
string internalCookieToken = context.Request.Cookies[HttpHeader.X_CSRF_TOKEN_COOKIE]?.Value;
27+
if (httpMethod == HttpMethod.Get.Method && (string.IsNullOrEmpty(tokens) || string.IsNullOrEmpty(internalCookieToken)))
28+
{
29+
AntiForgery.GetTokens(null, out cookieToken, out formToken);
30+
#pragma warning disable SCS0009 // The cookie is missing security flag HttpOnly
31+
HttpCookie cookie = new HttpCookie(HttpHeader.X_CSRF_TOKEN_COOKIE, formToken)
32+
{
33+
HttpOnly = false,
34+
Secure = GxContext.GetHttpSecure(context) == 1,
35+
};
36+
#pragma warning restore SCS0009 // The cookie is missing security flag HttpOnly
37+
HttpCookie internalCookie = new HttpCookie(AntiForgeryConfig.CookieName, cookieToken)
38+
{
39+
HttpOnly = true,
40+
Secure = GxContext.GetHttpSecure(context) == 1,
41+
};
42+
context.Response.SetCookie(cookie);
43+
context.Response.SetCookie(internalCookie);
44+
}
45+
if (httpMethod == HttpMethod.Delete.Method || httpMethod == HttpMethod.Post.Method || httpMethod == HttpMethod.Put.Method)
46+
{
47+
cookieToken = context.Request.Cookies[AntiForgeryConfig.CookieName]?.Value;
48+
string headerToken = context.Request.Headers[HttpHeader.X_CSRF_TOKEN_HEADER];
49+
AntiForgery.Validate(cookieToken, headerToken);
50+
}
51+
}
52+
}
53+
}

dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public class HttpHeader
5050
public static string XGXFILENAME = "x-gx-filename";
5151
internal static string ACCEPT = "Accept";
5252
internal static string TRANSFER_ENCODING = "Transfer-Encoding";
53-
internal static string X_GXCSRF_TOKEN = "X-GXCSRF-TOKEN";
53+
internal static string X_CSRF_TOKEN_HEADER = "X-XSRF-TOKEN";
54+
internal static string X_CSRF_TOKEN_COOKIE = "XSRF-TOKEN";
5455
}
5556
internal class HttpHeaderValue
5657
{

dotnet/src/dotnetframework/GxClasses/Middleware/GXHttpModules.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,11 @@ private void onPostResolveRequestCache(object sender, EventArgs eventArgs)
8686
if (apiHandler != null)
8787
HttpContext.Current.RemapHandler(apiHandler);
8888
}
89+
else if (string.Equals(HttpContext.Current.Request.HttpMethod, HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase) &&
90+
HttpContext.Current.Request.Path.EndsWith("/" + REST_BASE_URL, StringComparison.OrdinalIgnoreCase))
91+
{
92+
CSRFHelper.ValidateAntiforgery(HttpContext.Current);
93+
}
8994
}
9095
void IHttpModule.Dispose()
9196
{

dotnet/src/dotnetframework/GxClasses/Services/GXRestServices.cs

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -558,11 +558,8 @@ void AddHeader(string header, string value)
558558
[SecuritySafeCritical]
559559
public bool ProcessHeaders(string queryId)
560560
{
561-
if (RestAPIHelpers.ValidateCsrfToken())
562-
{
563-
ValidateAntiforgery();
564-
}
565-
561+
CSRFHelper.ValidateAntiforgery(context.HttpContext);
562+
566563
NameValueCollection headers = GetHeaders();
567564
String language = null, theme = null, etag = null;
568565
if (headers != null)
@@ -601,38 +598,7 @@ public bool ProcessHeaders(string queryId)
601598
return true;
602599
}
603600

604-
[SecurityCritical]
605-
private void ValidateAntiforgery()
606-
{
607-
string cookieToken, formToken;
608-
string httpMethod = context.HttpContext.Request.HttpMethod;
609-
string tokens = context.HttpContext.Request.Cookies[HttpHeader.X_GXCSRF_TOKEN]?.Value;
610-
string internalCookieToken = context.HttpContext.Request.Cookies[HttpHeader.X_GXCSRF_TOKEN]?.Value;
611-
if (httpMethod == HttpMethod.Get.Method && (string.IsNullOrEmpty(tokens) || string.IsNullOrEmpty(internalCookieToken)))
612-
{
613-
AntiForgery.GetTokens(null, out cookieToken, out formToken);
614-
#pragma warning disable SCS0009 // The cookie is missing security flag HttpOnly
615-
HttpCookie cookie = new HttpCookie(HttpHeader.X_GXCSRF_TOKEN, formToken)
616-
{
617-
HttpOnly = false,
618-
Secure = context.GetHttpSecure() == 1,
619-
};
620-
#pragma warning restore SCS0009 // The cookie is missing security flag HttpOnly
621-
HttpCookie internalCookie = new HttpCookie(AntiForgeryConfig.CookieName, cookieToken)
622-
{
623-
HttpOnly = true,
624-
Secure = context.GetHttpSecure() == 1,
625-
};
626-
context.HttpContext.Response.SetCookie(cookie);
627-
context.HttpContext.Response.SetCookie(internalCookie);
628-
}
629-
if (httpMethod == HttpMethod.Delete.Method || httpMethod == HttpMethod.Post.Method || httpMethod == HttpMethod.Put.Method)
630-
{
631-
cookieToken = context.HttpContext.Request.Cookies[AntiForgeryConfig.CookieName]?.Value;
632-
string headerToken = context.HttpContext.Request.Headers[HttpHeader.X_GXCSRF_TOKEN];
633-
AntiForgery.Validate(cookieToken, headerToken);
634-
}
635-
}
601+
636602

637603
private void SendCacheHeaders()
638604
{

dotnet/test/DotNetCoreAttackMitigationTest/Middleware/RestServiceTest.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ public async Task RunController()
5353
foreach (var item in SetCookieHeaderValue.ParseList(values.ToList()))
5454
cookies.Add(requestUriObj, new Cookie(item.Name.Value, item.Value.Value, item.Path.Value));
5555

56-
var setCookie = SetCookieHeaderValue.ParseList(values.ToList()).FirstOrDefault(t => t.Name.Equals(HttpHeader.X_GXCSRF_TOKEN, StringComparison.OrdinalIgnoreCase));
56+
var setCookie = SetCookieHeaderValue.ParseList(values.ToList()).FirstOrDefault(t => t.Name.Equals(HttpHeader.X_CSRF_TOKEN_COOKIE, StringComparison.OrdinalIgnoreCase));
5757
csrfToken = setCookie.Value.Value;
5858

5959
response.EnsureSuccessStatusCode();
6060
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode); //When failed, turn on log.config to see server side error.
6161

6262
StringContent body = new StringContent("{\"Image\":\"imageName\",\"ImageDescription\":\"imageDescription\"}");
63-
client.DefaultRequestHeaders.Add(HttpHeader.X_GXCSRF_TOKEN, csrfToken);
63+
client.DefaultRequestHeaders.Add(HttpHeader.X_CSRF_TOKEN_HEADER, csrfToken);
6464
client.DefaultRequestHeaders.Add("Cookie", values);// //cookies.GetCookieHeader(requestUriObj));
6565

6666
response = await client.PostAsync("rest/apps/saveimage", body);
@@ -74,7 +74,7 @@ public async Task HttpFirstPost()
7474
IEnumerable<string> cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value;
7575
foreach (string cookie in cookies)
7676
{
77-
Assert.False(cookie.StartsWith(HttpHeader.X_GXCSRF_TOKEN));
77+
Assert.False(cookie.StartsWith(HttpHeader.X_CSRF_TOKEN_COOKIE));
7878
}
7979
response.EnsureSuccessStatusCode();
8080
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
@@ -87,7 +87,7 @@ public async Task HttpFirstGet()
8787
IEnumerable<string> cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value;
8888
foreach (string cookie in cookies)
8989
{
90-
Assert.False(cookie.StartsWith(HttpHeader.X_GXCSRF_TOKEN));
90+
Assert.False(cookie.StartsWith(HttpHeader.X_CSRF_TOKEN_COOKIE));
9191
}
9292

9393
response.EnsureSuccessStatusCode();

0 commit comments

Comments
 (0)