Skip to content

Commit

Permalink
Bind CSRF token to session id for .NET.
Browse files Browse the repository at this point in the history
Move VerificationToken service to root path (rest/)
  • Loading branch information
claudiamurialdo committed Sep 7, 2023
1 parent 4959fe1 commit d4886c3
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 113 deletions.
83 changes: 83 additions & 0 deletions dotnet/src/dotnetcore/GxNetCoreStartup/CsrfHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@


using System;
using System.Threading.Tasks;
using GeneXus.Configuration;
using GeneXus.Http;
using log4net;
using Microsoft.AspNetCore.Antiforgery;
using Microsoft.AspNetCore.Http;
namespace GeneXus.Application
{
public class ValidateAntiForgeryTokenMiddleware
{
static readonly ILog log = log4net.LogManager.GetLogger(typeof(ValidateAntiForgeryTokenMiddleware));

private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
private string _basePath;

public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery, String basePath)
{
_next = next;
_antiforgery = antiforgery;
_basePath = "/" + basePath;
}

public async Task Invoke(HttpContext context)
{
if (context.Request.Path.HasValue && context.Request.Path.Value.StartsWith(_basePath))
{
if (HttpMethods.IsPost(context.Request.Method) ||
HttpMethods.IsDelete(context.Request.Method) ||
HttpMethods.IsPut(context.Request.Method))
{
string cookieToken = context.Request.Cookies[HttpHeader.X_CSRF_TOKEN_COOKIE];
string headerToken = context.Request.Headers[HttpHeader.X_CSRF_TOKEN_HEADER];
GXLogging.Debug(log, $"Antiforgery validation, cookieToken:{cookieToken}, headerToken:{headerToken}");

await _antiforgery.ValidateRequestAsync(context);
GXLogging.Debug(log, $"Antiforgery validation OK");
}
else if (HttpMethods.IsGet(context.Request.Method))
{
SetAntiForgeryTokens(_antiforgery, context);
}
}
if (!context.Request.Path.Value.EndsWith(_basePath)) //VerificationToken
await _next(context);
}
internal static void SetAntiForgeryTokens(IAntiforgery _antiforgery, HttpContext context)
{
AntiforgeryTokenSet tokenSet = _antiforgery.GetAndStoreTokens(context);
string sameSite;
CookieOptions cookieOptions = new CookieOptions { HttpOnly = false, Secure = GxContext.GetHttpSecure(context) == 1 };
SameSiteMode sameSiteMode = SameSiteMode.Unspecified;
if (Config.GetValueOf("SAMESITE_COOKIE", out sameSite) && Enum.TryParse(sameSite, out sameSiteMode))
{
cookieOptions.SameSite = sameSiteMode;
}
context.Response.Cookies.Append(HttpHeader.X_CSRF_TOKEN_COOKIE, tokenSet.RequestToken, cookieOptions);
GXLogging.Debug(log, $"Setting cookie ", HttpHeader.X_CSRF_TOKEN_COOKIE, "=", tokenSet.RequestToken, " samesite:" + sameSiteMode);
}

}
public class SessionIdAntiforgeryAdditionalDataProvider : IAntiforgeryAdditionalDataProvider
{
static readonly ILog log = log4net.LogManager.GetLogger(typeof(SessionIdAntiforgeryAdditionalDataProvider));
public string GetAdditionalData(HttpContext context)
{
context.NewSessionCheck();
GXLogging.Debug(log, $"Setting session id as additional CSRF token data:", context.Session.Id);
return context.Session.Id.Trim();
}

public bool ValidateAdditionalData(HttpContext context, string additionalData)
{
bool validSession = context.Session.Id.Trim().CompareTo(additionalData.Trim()) == 0 ? true : false;
GXLogging.Warn(log, $"Session id in CSRF token ({additionalData}) does not match the current session id ({context.Session.Id})");
return validSession;
}
}

}
74 changes: 3 additions & 71 deletions dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@


using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Azure.Identity;
using Azure.Monitor.OpenTelemetry.Exporter;
Expand Down Expand Up @@ -253,9 +250,10 @@ public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery(options =>
{
options.HeaderName = HttpHeader.X_GXCSRF_TOKEN;
options.HeaderName = HttpHeader.X_CSRF_TOKEN_HEADER;
options.SuppressXFrameOptionsHeader = false;
});
services.AddSingleton<IAntiforgeryAdditionalDataProvider, SessionIdAntiforgeryAdditionalDataProvider>();
}
services.AddDirectoryBrowser();
if (GXUtil.CompressResponse())
Expand Down Expand Up @@ -460,20 +458,10 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
routes.MapRoute($"{s}", new RequestDelegate(gxRouting.ProcessRestRequest));
}
}
routes.MapRoute($"{restBasePath}VerificationToken", (context) =>
{
string requestPath = context.Request.Path.Value;

if (string.Equals(requestPath, $"/{restBasePath}VerificationToken", StringComparison.OrdinalIgnoreCase) && antiforgery!=null)
{
ValidateAntiForgeryTokenMiddleware.SetAntiForgeryTokens(antiforgery, context);
}
return Task.CompletedTask;
});
routes.MapRoute($"{restBasePath}{{*{UrlTemplateControllerWithParms}}}", new RequestDelegate(gxRouting.ProcessRestRequest));
routes.MapRoute("Default", VirtualPath, new { controller = "Home", action = "Index" });
});

app.UseWebSockets();
string basePath = string.IsNullOrEmpty(VirtualPath) ? string.Empty : $"/{VirtualPath}";
Config.ScriptPath = basePath;
Expand Down Expand Up @@ -630,60 +618,4 @@ public IActionResult Index()
return Redirect(defaultFiles[0]);
}
}
public class ValidateAntiForgeryTokenMiddleware
{
static readonly ILog log = log4net.LogManager.GetLogger(typeof(ValidateAntiForgeryTokenMiddleware));

private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
private string _basePath;

public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery, String basePath)
{
_next = next;
_antiforgery = antiforgery;
_basePath = "/" + basePath;
}

public async Task Invoke(HttpContext context)
{
if (context.Request.Path.HasValue && context.Request.Path.Value.StartsWith(_basePath))
{
if (HttpMethods.IsPost(context.Request.Method) ||
HttpMethods.IsDelete(context.Request.Method) ||
HttpMethods.IsPut(context.Request.Method))
{
string cookieToken = context.Request.Cookies[HttpHeader.X_GXCSRF_TOKEN];
string headerToken = context.Request.Headers[HttpHeader.X_GXCSRF_TOKEN];
GXLogging.Debug(log, $"Antiforgery validation, cookieToken:{cookieToken}, headerToken:{headerToken}");

await _antiforgery.ValidateRequestAsync(context);
GXLogging.Debug(log, $"Antiforgery validation OK");
}
else if (HttpMethods.IsGet(context.Request.Method))
{
string tokens = context.Request.Cookies[HttpHeader.X_GXCSRF_TOKEN];
if (string.IsNullOrEmpty(tokens))
{
SetAntiForgeryTokens(_antiforgery, context);
}
}
}
await _next(context);
}
internal static void SetAntiForgeryTokens(IAntiforgery _antiforgery, HttpContext context)
{
AntiforgeryTokenSet tokenSet = _antiforgery.GetAndStoreTokens(context);
string sameSite;
CookieOptions cookieOptions = new CookieOptions { HttpOnly = false, Secure = GxContext.GetHttpSecure(context) == 1 };
SameSiteMode sameSiteMode= SameSiteMode.Unspecified;
if (Config.GetValueOf("SAMESITE_COOKIE", out sameSite) && Enum.TryParse(sameSite, out sameSiteMode))
{
cookieOptions.SameSite = sameSiteMode;
}
context.Response.Cookies.Append(HttpHeader.X_GXCSRF_TOKEN, tokenSet.RequestToken, cookieOptions);
GXLogging.Debug(log, $"Setting cookie ", HttpHeader.X_GXCSRF_TOKEN, "=", tokenSet.RequestToken, " samesite:" + sameSiteMode);
}

}
}
74 changes: 74 additions & 0 deletions dotnet/src/dotnetframework/GxClasses/Helpers/CsrfHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
using System.Net.Http;
using System.Security;
using System.Web;
using System.Web.Helpers;
using GeneXus.Application;
using GeneXus.Utils;
using log4net;

namespace GeneXus.Http
{
internal class CSRFHelper
{
//AntiForgeryConfig.AdditionalDataProvider = new SessionIdAntiforgeryAdditionalDataProvider();

[SecuritySafeCritical]
internal static void ValidateAntiforgery(HttpContext context)
{
if (RestAPIHelpers.ValidateCsrfToken())
{
ValidateAntiforgeryImpl(context);
}
}
[SecurityCritical]
static void ValidateAntiforgeryImpl(HttpContext context)
{
string cookieToken, formToken;
string httpMethod = context.Request.HttpMethod;
string tokens = context.Request.Cookies[HttpHeader.X_CSRF_TOKEN_COOKIE]?.Value;
string internalCookieToken = context.Request.Cookies[HttpHeader.X_CSRF_TOKEN_COOKIE]?.Value;
if (httpMethod == HttpMethod.Get.Method && (string.IsNullOrEmpty(tokens) || string.IsNullOrEmpty(internalCookieToken)))
{
AntiForgery.GetTokens(null, out cookieToken, out formToken);
#pragma warning disable SCS0009 // The cookie is missing security flag HttpOnly
HttpCookie cookie = new HttpCookie(HttpHeader.X_CSRF_TOKEN_COOKIE, formToken)
{
HttpOnly = false,
Secure = GxContext.GetHttpSecure(context) == 1,
};
#pragma warning restore SCS0009 // The cookie is missing security flag HttpOnly
HttpCookie internalCookie = new HttpCookie(AntiForgeryConfig.CookieName, cookieToken)
{
HttpOnly = true,
Secure = GxContext.GetHttpSecure(context) == 1,
};
context.Response.SetCookie(cookie);
context.Response.SetCookie(internalCookie);
}
if (httpMethod == HttpMethod.Delete.Method || httpMethod == HttpMethod.Post.Method || httpMethod == HttpMethod.Put.Method)
{
cookieToken = context.Request.Cookies[AntiForgeryConfig.CookieName]?.Value;
string headerToken = context.Request.Headers[HttpHeader.X_CSRF_TOKEN_HEADER];
AntiForgery.Validate(cookieToken, headerToken);
}
}
}


public class SessionIdAntiforgeryAdditionalDataProvider : IAntiForgeryAdditionalDataProvider
{
static readonly ILog log = log4net.LogManager.GetLogger(typeof(SessionIdAntiforgeryAdditionalDataProvider));
public string GetAdditionalData(HttpContextBase context)
{
GXLogging.Debug(log, $"Setting session id as additional CSRF token data:", context.Session.SessionID);
return context.Session.SessionID.Trim();
}
[SecuritySafeCritical]
public bool ValidateAdditionalData(HttpContextBase context, string additionalData)
{
bool validSession = context.Session.SessionID.Trim().CompareTo(additionalData.Trim()) == 0 ? true : false;
GXLogging.Warn(log, $"Session id in CSRF token ({additionalData}) does not match the current session id ({context.Session.SessionID})");
return validSession;
}
}
}
3 changes: 2 additions & 1 deletion dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ public class HttpHeader
public static string XGXFILENAME = "x-gx-filename";
internal static string ACCEPT = "Accept";
internal static string TRANSFER_ENCODING = "Transfer-Encoding";
internal static string X_GXCSRF_TOKEN = "X-GXCSRF-TOKEN";
internal static string X_CSRF_TOKEN_HEADER = "X-XSRF-TOKEN";
internal static string X_CSRF_TOKEN_COOKIE = "XSRF-TOKEN";
}
internal class HttpHeaderValue
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ private void onPostResolveRequestCache(object sender, EventArgs eventArgs)
if (apiHandler != null)
HttpContext.Current.RemapHandler(apiHandler);
}
else if (string.Equals(HttpContext.Current.Request.HttpMethod, HttpMethod.Get.Method, StringComparison.OrdinalIgnoreCase) &&
HttpContext.Current.Request.Path.EndsWith("/" + REST_BASE_URL, StringComparison.OrdinalIgnoreCase))
{
CSRFHelper.ValidateAntiforgery(HttpContext.Current);
}
}
void IHttpModule.Dispose()
{
Expand Down
40 changes: 3 additions & 37 deletions dotnet/src/dotnetframework/GxClasses/Services/GXRestServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -558,11 +558,8 @@ void AddHeader(string header, string value)
[SecuritySafeCritical]
public bool ProcessHeaders(string queryId)
{
if (RestAPIHelpers.ValidateCsrfToken())
{
ValidateAntiforgery();
}

CSRFHelper.ValidateAntiforgery(context.HttpContext);

NameValueCollection headers = GetHeaders();
String language = null, theme = null, etag = null;
if (headers != null)
Expand Down Expand Up @@ -601,38 +598,7 @@ public bool ProcessHeaders(string queryId)
return true;
}

[SecurityCritical]
private void ValidateAntiforgery()
{
string cookieToken, formToken;
string httpMethod = context.HttpContext.Request.HttpMethod;
string tokens = context.HttpContext.Request.Cookies[HttpHeader.X_GXCSRF_TOKEN]?.Value;
string internalCookieToken = context.HttpContext.Request.Cookies[HttpHeader.X_GXCSRF_TOKEN]?.Value;
if (httpMethod == HttpMethod.Get.Method && (string.IsNullOrEmpty(tokens) || string.IsNullOrEmpty(internalCookieToken)))
{
AntiForgery.GetTokens(null, out cookieToken, out formToken);
#pragma warning disable SCS0009 // The cookie is missing security flag HttpOnly
HttpCookie cookie = new HttpCookie(HttpHeader.X_GXCSRF_TOKEN, formToken)
{
HttpOnly = false,
Secure = context.GetHttpSecure() == 1,
};
#pragma warning restore SCS0009 // The cookie is missing security flag HttpOnly
HttpCookie internalCookie = new HttpCookie(AntiForgeryConfig.CookieName, cookieToken)
{
HttpOnly = true,
Secure = context.GetHttpSecure() == 1,
};
context.HttpContext.Response.SetCookie(cookie);
context.HttpContext.Response.SetCookie(internalCookie);
}
if (httpMethod == HttpMethod.Delete.Method || httpMethod == HttpMethod.Post.Method || httpMethod == HttpMethod.Put.Method)
{
cookieToken = context.HttpContext.Request.Cookies[AntiForgeryConfig.CookieName]?.Value;
string headerToken = context.HttpContext.Request.Headers[HttpHeader.X_GXCSRF_TOKEN];
AntiForgery.Validate(cookieToken, headerToken);
}
}


private void SendCacheHeaders()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ public async Task RunController()
foreach (var item in SetCookieHeaderValue.ParseList(values.ToList()))
cookies.Add(requestUriObj, new Cookie(item.Name.Value, item.Value.Value, item.Path.Value));

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

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

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

response = await client.PostAsync("rest/apps/saveimage", body);
Expand All @@ -74,7 +74,7 @@ public async Task HttpFirstPost()
IEnumerable<string> cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value;
foreach (string cookie in cookies)
{
Assert.False(cookie.StartsWith(HttpHeader.X_GXCSRF_TOKEN));
Assert.False(cookie.StartsWith(HttpHeader.X_CSRF_TOKEN_COOKIE));
}
response.EnsureSuccessStatusCode();
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
Expand All @@ -87,7 +87,7 @@ public async Task HttpFirstGet()
IEnumerable<string> cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value;
foreach (string cookie in cookies)
{
Assert.False(cookie.StartsWith(HttpHeader.X_GXCSRF_TOKEN));
Assert.False(cookie.StartsWith(HttpHeader.X_CSRF_TOKEN_COOKIE));
}

response.EnsureSuccessStatusCode();
Expand Down

0 comments on commit d4886c3

Please sign in to comment.