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

Extend CSRF validation to all backend services #917

Merged
merged 5 commits into from
Dec 27, 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
12 changes: 9 additions & 3 deletions dotnet/src/dotnetcore/GxNetCoreStartup/CsrfHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ public class ValidateAntiForgeryTokenMiddleware
static readonly IGXLogger log = GXLoggerFactory.GetLogger<ValidateAntiForgeryTokenMiddleware>();
private readonly RequestDelegate _next;
private readonly IAntiforgery _antiforgery;
private string _restBasePath;
private string _basePath;

public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery, String basePath)
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery, string basePath)
{
_next = next;
_antiforgery = antiforgery;
_basePath = "/" + basePath;
_restBasePath = $"{basePath}{Startup.REST_BASE_URL}";
_basePath = $"/{basePath}";
}

public async Task Invoke(HttpContext context)
Expand All @@ -42,9 +44,13 @@ public async Task Invoke(HttpContext context)
SetAntiForgeryTokens(_antiforgery, context);
}
}
if (!context.Request.Path.Value.EndsWith(_basePath)) //VerificationToken
if (!IsVerificationTokenServiceRequest(context))
await _next(context);
}
private bool IsVerificationTokenServiceRequest(HttpContext context)
{
return context.Request.Path.Value.EndsWith(_restBasePath);
}
internal static void SetAntiForgeryTokens(IAntiforgery _antiforgery, HttpContext context)
{
AntiforgeryTokenSet tokenSet = _antiforgery.GetAndStoreTokens(context);
Expand Down
8 changes: 3 additions & 5 deletions dotnet/src/dotnetcore/GxNetCoreStartup/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public class Startup
const string RESOURCES_FOLDER = "Resources";
const string TRACE_FOLDER = "logs";
const string TRACE_PATTERN = "trace.axd";
const string REST_BASE_URL = "rest/";
internal const string REST_BASE_URL = "rest/";
const string DATA_PROTECTION_KEYS = "DataProtection-Keys";
const string REWRITE_FILE = "rewrite.config";
const string SWAGGER_DEFAULT_YAML = "default.yaml";
Expand Down Expand Up @@ -411,7 +411,7 @@ public void Configure(IApplicationBuilder app, Microsoft.AspNetCore.Hosting.IHos
if (RestAPIHelpers.ValidateCsrfToken())
{
antiforgery = app.ApplicationServices.GetRequiredService<IAntiforgery>();
app.UseAntiforgeryTokens(restBasePath);
app.UseAntiforgeryTokens(apiBasePath);
}
app.UseMvc(routes =>
{
Expand Down Expand Up @@ -501,7 +501,6 @@ bool IsAspx(HttpContext context, string basePath)
}
public class CustomExceptionHandlerMiddleware
{
const string InvalidCSRFToken = "InvalidCSRFToken";
static readonly IGXLogger log = GXLoggerFactory.GetLogger<CustomExceptionHandlerMiddleware>();
public async Task Invoke(HttpContext httpContext)
{
Expand All @@ -516,9 +515,8 @@ public async Task Invoke(HttpContext httpContext)
}
else if (ex is AntiforgeryValidationException)
{
//"The required antiforgery header value "X-GXCSRF-TOKEN" is not present.
httpStatusCode = HttpStatusCode.BadRequest;
httpReasonPhrase = InvalidCSRFToken;
httpReasonPhrase = HttpHelper.InvalidCSRFToken;
GXLogging.Error(log, $"Validation of antiforgery failed", ex);
}
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ public static Dictionary<string, object> ReadRestBodyParameters(Stream stream)

internal static bool ValidateCsrfToken()
{
return Config.GetValueOf("ValidateCSRF", Preferences.NO) == Preferences.YES;
return Config.GetValueOf("CSRF_PROTECTION", Preferences.NO) == Preferences.YES;
}
}
}
8 changes: 6 additions & 2 deletions dotnet/src/dotnetframework/GxClasses/Helpers/HttpHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public class HttpHelper
const string GAM_CODE_TFA_USER_MUST_VALIDATE = "410";
const string GAM_CODE_TOKEN_EXPIRED = "103";
static Regex CapitalsToTitle = new Regex(@"(?<=[A-Z])(?=[A-Z][a-z]) | (?<=[^A-Z])(?=[A-Z]) | (?<=[A-Za-z])(?=[^A-Za-z])", RegexOptions.IgnorePatternWhitespace);

internal const string InvalidCSRFToken = "InvalidCSRFToken";
const string CORS_MAX_AGE_SECONDS = "86400";
internal static void CorsHeaders(HttpContext httpContext)
{
Expand Down Expand Up @@ -286,10 +286,14 @@ internal static void TraceUnexpectedError(Exception ex)
}

internal static void SetUnexpectedError(HttpContext httpContext, HttpStatusCode statusCode, Exception ex)
{
string statusCodeDesc = StatusCodeToTitle(statusCode);
SetUnexpectedError(httpContext, statusCode, statusCodeDesc, ex);
}
internal static void SetUnexpectedError(HttpContext httpContext, HttpStatusCode statusCode, string statusCodeDesc, Exception ex)
{
TraceUnexpectedError(ex);
string statusCodeStr = statusCode.ToString(INT_FORMAT);
string statusCodeDesc = StatusCodeToTitle(statusCode);
SetResponseStatus(httpContext, statusCode, statusCodeDesc);
SetJsonError(httpContext, statusCodeStr, statusCodeDesc);
}
Expand Down
31 changes: 23 additions & 8 deletions dotnet/src/dotnetframework/GxClasses/Middleware/GXHttp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,17 @@ namespace GeneXus.Http
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.Script.Serialization;
using System.Net;
using GeneXus.Notifications;
using Web.Security;
using System.Web.SessionState;
using GeneXus.Mock;
using GeneXus.Data.NTier;
#endif
using System.Web.Mvc;
using System.Security;



#endif
#if NETCORE
public abstract class GXHttpHandler : GXBaseObject, IHttpHandler
#else
Expand Down Expand Up @@ -1905,8 +1905,9 @@ public bool IsMain
get { return _isMain; }
}
#endif


#if !NETCORE
[SecuritySafeCritical]
#endif
public void ProcessRequest(HttpContext httpContext)
{
localHttpContext = httpContext;
Expand Down Expand Up @@ -1934,6 +1935,9 @@ public void ProcessRequest(HttpContext httpContext)
SetStreaming();
SendHeaders();
string clientid = context.ClientID; //Send clientid cookie (before response HasStarted) if necessary, since UseResponseBuffering is not in .netcore3.0
#if !NETCORE
CSRFHelper.ValidateAntiforgery(httpContext);
#endif

bool validSession = ValidWebSession();
if (validSession && IntegratedSecurityEnabled)
Expand Down Expand Up @@ -1978,9 +1982,20 @@ public void ProcessRequest(HttpContext httpContext)
context.CloseConnections();
}
catch { }
Exception exceptionToHandle = e.InnerException ?? e;
handleException(exceptionToHandle.GetType().FullName, exceptionToHandle.Message, exceptionToHandle.StackTrace);
throw new Exception("GXApplication exception", e);
#if !NETCORE
if (e is HttpAntiForgeryException)
{
httpContext.Response.StatusCode = (int)HttpStatusCode.BadRequest;
httpContext.Response.StatusDescription = HttpHelper.InvalidCSRFToken;
GXLogging.Error(log, $"Validation of antiforgery failed", e);
}
else
#endif
{
Exception exceptionToHandle = e.InnerException ?? e;
handleException(exceptionToHandle.GetType().FullName, exceptionToHandle.Message, exceptionToHandle.StackTrace);
throw new Exception("GXApplication exception", e);
}
}
}
protected virtual bool ChunkedStreaming() { return false; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,10 +389,14 @@ public void WebException(Exception ex)
{
throw ex;
}
else if (ex is FormatException || (RestAPIHelpers.ValidateCsrfToken() && AntiForgeryException(ex)))
else if (ex is FormatException)
{
HttpHelper.SetUnexpectedError(httpContext, HttpStatusCode.BadRequest, ex);
}
else if (RestAPIHelpers.ValidateCsrfToken() && AntiForgeryException(ex))
{
HttpHelper.SetUnexpectedError(httpContext, HttpStatusCode.BadRequest, HttpHelper.InvalidCSRFToken, ex);
}
else
{
HttpHelper.SetUnexpectedError(httpContext, HttpStatusCode.InternalServerError, ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,21 +24,19 @@ public RestServiceTest() : base()
server.AllowSynchronousIO = true;

}

public async Task TestSimpleRestPost()
[Fact]
public async Task InvalidPostToRestService()
{
server.AllowSynchronousIO = true;
HttpClient client = server.CreateClient();
StringContent body = new StringContent("{\"Image\":\"imageName\",\"ImageDescription\":\"imageDescription\"}");
HttpResponseMessage response = await client.PostAsync("rest/apps/saveimage", body);
Assert.Equal(System.Net.HttpStatusCode.BadRequest, response.StatusCode);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}

public async Task RunController()
[Fact]
public async Task ValidPostToRestService()
{


CookieContainer cookies = new System.Net.CookieContainer();
CookieContainer cookies = new CookieContainer();
HttpClient client = server.CreateClient();
string requestUri = "rest/apps/append";
Uri requestUriObj = new Uri("http://localhost/" + requestUri);
Expand All @@ -51,43 +49,76 @@ 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_CSRF_TOKEN_COOKIE, StringComparison.OrdinalIgnoreCase));
SetCookieHeaderValue setCookie = SetCookieHeaderValue.ParseList(values.ToList()).FirstOrDefault(t => t.Name.Equals(HttpHeader.X_CSRF_TOKEN_COOKIE, StringComparison.OrdinalIgnoreCase));
Assert.NotNull(setCookie);
Assert.True(setCookie.Value.HasValue);
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.
Assert.Equal(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_CSRF_TOKEN_HEADER, csrfToken);
client.DefaultRequestHeaders.Add("Cookie", values);// //cookies.GetCookieHeader(requestUriObj));

response = await client.PostAsync("rest/apps/saveimage", body);
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
[Fact]
public async Task ValidPostToHttpService()
{
CookieContainer cookies = new CookieContainer();
HttpClient client = server.CreateClient();
string requestUri = "webhook.aspx";
Uri requestUriObj = new Uri("http://localhost/" + requestUri);
HttpResponseMessage response = await client.GetAsync(requestUri);
string csrfToken = string.Empty;

IEnumerable<string> values;
Assert.True(response.Headers.TryGetValues("Set-Cookie", out values));

foreach (var item in SetCookieHeaderValue.ParseList(values.ToList()))
cookies.Add(requestUriObj, new Cookie(item.Name.Value, item.Value.Value, item.Path.Value));

SetCookieHeaderValue setCookie = SetCookieHeaderValue.ParseList(values.ToList()).FirstOrDefault(t => t.Name.Equals(HttpHeader.X_CSRF_TOKEN_COOKIE, StringComparison.OrdinalIgnoreCase));
Assert.NotNull(setCookie);
Assert.True(setCookie.Value.HasValue);
csrfToken = setCookie.Value.Value;

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

client.DefaultRequestHeaders.Add(HttpHeader.X_CSRF_TOKEN_HEADER, csrfToken);
client.DefaultRequestHeaders.Add("Cookie", values);// //cookies.GetCookieHeader(requestUriObj));

response = await client.PostAsync(requestUri, null);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
public async Task HttpFirstPost()
[Fact]
public async Task InvalidPostToHttpService()
{
HttpClient client = server.CreateClient();
HttpResponseMessage response = await client.PostAsync("webhook.aspx", null);
IEnumerable<string> cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value;
foreach (string cookie in cookies)
if (cookies != null)
{
Assert.False(cookie.StartsWith(HttpHeader.X_CSRF_TOKEN_COOKIE));
foreach (string cookie in cookies)
{
Assert.False(cookie.StartsWith(HttpHeader.X_CSRF_TOKEN_COOKIE));
}
}
response.EnsureSuccessStatusCode();
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
Assert.Contains("InvalidCSRFToken", response.ReasonPhrase, StringComparison.OrdinalIgnoreCase);
}
public async Task HttpFirstGet()
[Fact]
public async Task GetToHttpService()
{
HttpClient client = server.CreateClient();
HttpResponseMessage response = await client.GetAsync("webhook.aspx");
IEnumerable<string> cookies = response.Headers.SingleOrDefault(header => header.Key == "Set-Cookie").Value;
foreach (string cookie in cookies)
{
Assert.False(cookie.StartsWith(HttpHeader.X_CSRF_TOKEN_COOKIE));
}

Assert.Contains(cookies, x => x.StartsWith(HttpHeader.X_CSRF_TOKEN_COOKIE, StringComparison.OrdinalIgnoreCase));
response.EnsureSuccessStatusCode();
Assert.Equal(System.Net.HttpStatusCode.OK, response.StatusCode);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
"CACHE_INVALIDATION_TOKEN": "20216211291931",
"CORS_ALLOW_ORIGIN": "https://normal-website.com",
"MY_CUSTOM_PTY": "DEFAULT_VALUE",
"ValidateCSRF": "1"
"CSRF_PROTECTION": "1"
},
"languages": {
"English": {
Expand Down
Loading