Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bind CSRF token to session id for .NET. #872

Merged
merged 3 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions dotnet/src/dotnetcore/GxNetCoreStartup/CsrfHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@


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);
}

}
}
73 changes: 2 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,7 +250,7 @@ public void ConfigureServices(IServiceCollection services)
{
services.AddAntiforgery(options =>
{
options.HeaderName = HttpHeader.X_GXCSRF_TOKEN;
options.HeaderName = HttpHeader.X_CSRF_TOKEN_HEADER;
options.SuppressXFrameOptionsHeader = false;
});
}
Expand Down Expand Up @@ -460,20 +457,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 +617,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);
}

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

namespace GeneXus.Http
{
internal class CSRFHelper
{
[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);
}
}
}
}
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