I have just come across a rather strange issue and I really can't understand why this is happening ...
I have a rather simple, MVC website based on .NET Framework 4.7.2. I'm keeping 2 Resource files (resx) for a couple of languages. So far so good. What I do is keeping the selected Culture into a Cookie with the CultureInfo (en-US & el-GR). In my development maching, using IISExpress, everything is running like a charm! The cookie is being updated as expected and of course the value switching can be seen from the browser debugging.
Using Application_BeginRequest() from Global.asax I can recover the selected culture:
protected void Application_BeginRequest(object sender, EventArgs e)
{
string culture = "el-GR";
var langCookie = Request.Cookies["SiderLangCookie"];
if (langCookie != null)
culture = langCookie.Value;
else
{
culture = "el-GR";
HttpCookie cookie = new HttpCookie("SiderLangCookie", culture)
{
HttpOnly = true,
Expires = DateTime.Now.AddMonths(6)
};
Response.AddHeader("Set-Cookie", "SameSite=Strict;Secure");
Response.AppendCookie(cookie);
}
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(culture);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(culture);
}
And later on, if the user chooses to do so, he/she may change the culture from an anchor button:
<a class="socials-item" href="javascript:SwitchLanguage();" title="Language"><i class="fa fa-flag" aria-hidden="true"></i></a>
which is calling a javascript function for an AJAX POST request to the Controller action:
function SwitchLanguage() {
$.ajax({
url: '#Url.Action("SwitchLanguage", "Home")',
method: 'POST',
success: function (response) {
if (response.result == "OK") {
toastr.success("#Resource.LanguageSwitchSuccess", "SUCCESS");
setTimeout(function () { window.location.reload(); }, 2500);
}
},
error: function () {
toastr.error("#Resource.LanguageSwitchError", "ERROR");
}
});
}
and this is my action:
[HttpPost]
public ActionResult SwitchLanguage()
{
string lang = "en-US";
var langCookie = Request.Cookies["SiderLangCookie"];
if (langCookie == null)
{
langCookie = new HttpCookie("SiderLangCookie", lang)
{
HttpOnly = true,
Expires = DateTime.Now.AddMonths(6),
};
Response.AddHeader("Set-Cookie", "SameSite=Strict;Secure");
Response.AppendCookie(langCookie);
}
else
{
lang = langCookie.Value;
if (lang == "en-US")
lang = "el-GR";
else
lang = "en-US";
langCookie.Value = lang;
Response.AddHeader("Set-Cookie", "SameSite=Strict;Secure");
Response.SetCookie(langCookie);
}
Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo(lang);
Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo(lang);
return Json(new { result = "OK" }, JsonRequestBehavior.AllowGet);
}
For some reason, when I deploy the website (Publishing to a folder and uploading to the Host), even though the Action code is executing successfully (no exceptions and errors whatsoever), the cookie is no longer updating the value to either el-GR or en-US. It just sticks to the first value it got when first created.
Does anyone have the slightest clue why is this happening?
Thanks in advance.
Cookies issues usually appears if you do not have correct setup the relative sessions on web.config.
Check the domain that is correct setup with out the www. - if you use the www. then check that all calls done from www.
<httpCookies domain="yourdomain.com" httpOnlyCookies="true" requireSSL="true"/>
The domain property also exist on forms and on roleManager
Ok, I managed to figure out the situation. Many many thanks to Thomas Ardal who wrote this article:
[The ultimate guide to secure cookies with web.config in .NET][1]
[1]: https://blog.elmah.io/the-ultimate-guide-to-secure-cookies-with-web-config-in-net/
It seems that the latest changes regarding Cookie management by the browsers are more restrictive than anticipated.
Incorporating the rewrite rule, Thomas proposed, did the trick.
Now the Cookie value seems to be changing as expected.
Thanks
Related
We have an application which is already in production where an IDOR vulnerability was detected in an endpoint "CustomerKey". This contains customer ID which when used brute force allowed for session take over in our app.
Now we can't encrypt this as it might make the app slow as the CustomerKey is being used many times in the JS as well as backend. Our app allows many users to login at the same time we are using signalR in the app as well.
Some sample backend code where this CustomerKey is being used :
[HttpPost]
[AllowAnonymous]
public JsonResult UpLoadLog(string CustomerKey, string message)
{
try
{
var log = message.Split(new string[] { "\r\n" },
StringSplitOptions.RemoveEmptyEntries);
foreach (string s in log)
{
Logger.Write(LogType.Info, this.GetType(), "UpLoadLog", CustomerKey + ": "
+ s.TrimStart('\r', '\n'), null);
}
}
catch (Exception ex)
{
Logger.Write(LogType.Fatal, this.GetType(), "UpLoadLog", ex.Message, ex);
}
HttpContext.Response.StatusCode = (int)System.Net.HttpStatusCode.OK;
return null;
}
[AuthorizeUser]
public ActionResult Customer(string CustomerKey, string FeedData, string FeedInfo)
{
if (string.IsNullOrEmpty(CustomerKey))
{
var owinContext = Request.GetOwinContext();
CustomerKey = owinContext.Get<string>("CustomerKey");
FeedData = owinContext.Get<string>("FeedData");
FeedInfo = owinContext.Get<string>("FeedInfo");
}
ViewBag.CustomerKey = CustomerKey;
ViewBag.FeedData = FeedData;
ViewBag.FeedInfo = FeedInfo;
ViewBag.UseSignalR = true;
ViewBag.isOffline = true;
return View("Offline", "CustomerLayout");
}
Sample js code where CustomerKey is being used:
var UpLoadLog = function (log, isAsync) {
if (isAsync== null || isAsync== undefined)
isAsync= true;
jQuery.ajax({
type: "POST",
async: isAsync,
contentType: "application/json;charset=utf-8",
url: rooturl + "Authentication/UpLoadLog",
data: JSON.stringify({ CustomerKey: jQuery("#customerKey").val(), message: "\r\n\r\n" + log + "\r\n\r\n" }),
dataType: "json",
success: function (response, status, jqXHR) {
},
error: function (jqXHR, status, error) {
}
});
LogMessages = "\r\n\r\n";
};
The app also contains a hidden field in the layout where the value of CustomerKey is being used
<input type="hidden" id="customerkey" value="#ViewBag.CustomerKey"/>
What I need help is how can I resolve this vulnerability without making huge changes in the application?
If I understood the problem correctly, then it lies in the fact that anyone can send a request with any CustomerKey, and get a response by CustomerKey, even if they are not authorized to receive such information.
If the information about the CustomerKey is associated with authorization filters (AuthorizeUser), then, in my opinion, the least time-consuming way to solve the problem would be to compare the CustomerKey with the corresponding property of the authorized user. And if the keys don't match, throw an exception.
To talk about specific lines of code, I need, in fact, to work with the project and have access to all the source code.
You could enter a third parameter which is the md5() of the CustomerKey parameter. This way when the method is contacted, it checks if the md5 parameter is really the md5 of the CustomerKey parameter, if so it writes the log. It is a sort of checksum made on the first parameter which guarantees that only those who know this rule can save logs. This can be an example of the new method signature
[HttpPost]
[AllowAnonymous]
public JsonResult UpLoadLog(string CustomerKey, string message, string md5)
This instead a way to calculate md5:
using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create())
{
byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
byte[] hashBytes = md5.ComputeHash(inputBytes);
string calculatedMd5 = Convert.ToHexString(hashBytes);
}
Shortly I ll try to describe my problem.
When I try my api actions with Postman, Fiddler everything works fine. But when it comes to browser Ajax Request 'OPTIONS' seems to my API not handling 'OPTIONS' method.
By the way, my 'Get' AND 'Post' action works like a charm on browser.
My attempts :
Enable Cors (OK) tried either web.config or global.asax
I tried Application_BeginRequest to handle OPTIONS requests. It returns OK but did not pass to my main Path/Put function.
Notes : My app currently in development environment. So my service and client is on localhost with different ports. I am in doubt that its about deployment issue.
Technologies
Asp.Net Web Api, Odata for Service Layer and react for client presentation layer.
I would be appreciated to suggestion.
Thanks.
I've just solved the problem.
Solution : Set Auth and app.UseCors(corsOptions); config before app.useWebApi() code line.
Full config located below. Really sad...
Startup.cs
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
HttpConfiguration configuration = new HttpConfiguration();
//Config
Configure(app);
WebApiConfig.Register(configuration);
app.UseWebApi(configuration);
}
private void Configure(IAppBuilder app)
{
var corsPolicy = new CorsPolicy
{
AllowAnyMethod = true,
AllowAnyHeader = true,
SupportsCredentials = true
};
// Try and load allowed origins from web.config
// If none are specified we'll allow all origins
// TODO get this value from web.config
var origins = "http://localhost:5911";
if (origins != null)
{
foreach (var origin in origins.Split(';'))
{
corsPolicy.Origins.Add(origin);
}
}
else
{
corsPolicy.AllowAnyOrigin = true;
}
var corsOptions = new CorsOptions
{
PolicyProvider = new CorsPolicyProvider
{
PolicyResolver = context => Task.FromResult(corsPolicy)
}
};
app.UseCors(corsOptions);
OAuthAuthorizationServerOptions options = new OAuthAuthorizationServerOptions()
{
TokenEndpointPath = new Microsoft.Owin.PathString("/getToken"),
AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
AllowInsecureHttp = true,
Provider = new AuthorizationServerProvider() // Custom Provider
};
app.UseOAuthAuthorizationServer(options);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
I am trying to set the redirect_uri for the facebook login with Asp.Net Identity. However, the GetExternalLogin REST method in the AccountController is only triggered if the redirect_uri is '/'. If I add anything else it does not trigger GetExternalLogin, the browser only shows error: invalid_request.
However the url contains the redirected parameter as it should e.g. if I add the redirect_uri as http://localhost:25432/testing
the response URL looks like this:
http://localhost:25432/api/Account/ExternalLogin?provider=Facebook&response_type=token&client_id=self&redirect_uri=http%3A%2F%2Flocalhost%3A25432%2Ftesting&state=0NctHHGq_aiazEurHYbvJT8hDgl0GJ_GGSdFfq2z5SA1
and the browser window shows: error: invalid_request
Any idea why this works only when redirecting to '/' but not to any other urlĀ“s?
For anyone else that might run into this issue: the problem is when you take (copy) the ApplicationOAuthProvider.cs from the Visual Studio SPA template and it is there where this code is:
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
var expectedRootUri = new Uri(context.Request.Uri, "/");
if (expectedRootUri.AbsoluteUri == context.RedirectUri)
{
context.Validated();
}
}
return Task.FromResult<object>(null);
}
This will obviously block any redirect_uri that doesn't look like http://localhost/ or http://example.com/ so for instance http://example.com/home won't work.
Now this below is the source for InvokeAuthorizeEndpointAsync in Katana which does all the work and you can see it calls into any custom OAuthProvider that might be registered for this MVC/Web API application (this registration typically happens in Startup.Auth.cs):
private async Task<bool> InvokeAuthorizeEndpointAsync()
{
var authorizeRequest = new AuthorizeEndpointRequest(Request.Query);
var clientContext = new OAuthValidateClientRedirectUriContext(
Context,
Options,
authorizeRequest.ClientId,
authorizeRequest.RedirectUri);
if (!String.IsNullOrEmpty(authorizeRequest.RedirectUri))
{
bool acceptableUri = true;
Uri validatingUri;
if (!Uri.TryCreate(authorizeRequest.RedirectUri, UriKind.Absolute, out validatingUri))
{
// The redirection endpoint URI MUST be an absolute URI
// http://tools.ietf.org/html/rfc6749#section-3.1.2
acceptableUri = false;
}
else if (!String.IsNullOrEmpty(validatingUri.Fragment))
{
// The endpoint URI MUST NOT include a fragment component.
// http://tools.ietf.org/html/rfc6749#section-3.1.2
acceptableUri = false;
}
else if (!Options.AllowInsecureHttp &&
String.Equals(validatingUri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase))
{
// The redirection endpoint SHOULD require the use of TLS
// http://tools.ietf.org/html/rfc6749#section-3.1.2.1
acceptableUri = false;
}
if (!acceptableUri)
{
clientContext.SetError(Constants.Errors.InvalidRequest);
return await SendErrorRedirectAsync(clientContext, clientContext);
}
}
await Options.Provider.ValidateClientRedirectUri(clientContext);
if (!clientContext.IsValidated)
{
_logger.WriteVerbose("Unable to validate client information");
return await SendErrorRedirectAsync(clientContext, clientContext);
}
var validatingContext = new OAuthValidateAuthorizeRequestContext(
Context,
Options,
authorizeRequest,
clientContext);
if (string.IsNullOrEmpty(authorizeRequest.ResponseType))
{
_logger.WriteVerbose("Authorize endpoint request missing required response_type parameter");
validatingContext.SetError(Constants.Errors.InvalidRequest);
}
else if (!authorizeRequest.IsAuthorizationCodeGrantType &&
!authorizeRequest.IsImplicitGrantType)
{
_logger.WriteVerbose("Authorize endpoint request contains unsupported response_type parameter");
validatingContext.SetError(Constants.Errors.UnsupportedResponseType);
}
else
{
await Options.Provider.ValidateAuthorizeRequest(validatingContext);
}
if (!validatingContext.IsValidated)
{
// an invalid request is not processed further
return await SendErrorRedirectAsync(clientContext, validatingContext);
}
_clientContext = clientContext;
_authorizeEndpointRequest = authorizeRequest;
var authorizeEndpointContext = new OAuthAuthorizeEndpointContext(Context, Options);
await Options.Provider.AuthorizeEndpoint(authorizeEndpointContext);
return authorizeEndpointContext.IsRequestCompleted;
}
This is key:
await Options.Provider.ValidateClientRedirectUri(clientContext);
So your solution is to change how the ValidateClientRedirectUri performs the validation - the default SPA implementation is, as you can see, very naive.
There's lots of ppl having issues with SPA mainly because it lacks any kind of useful information and I mean that both for ASP.NET Identity and OWIN stuff and with regards to what is going on within KnockoutJS implementation.
I wish Microsoft would provide more comprehensive docs for these templates because anyone who will try to do anything a bit more complex will run into issues.
I've spent hours on this, digging into OWIN (Katana) source code thinking it is the above implementation that blocks my redirect URIs but it was not, hopefully helps someone else too.
HTH
The problem is that GetExternalLogin registered as OAuthOptions.AuthorizeEndpointPath which used for app.UseOAuthBearerTokens(OAuthOptions). This configuration puts validation on arguments of endpoint.
if (!Uri.TryCreate(authorizeRequest.RedirectUri, UriKind.Absolute, out validatingUri))
{
// The redirection endpoint URI MUST be an absolute URI
}
else if (!String.IsNullOrEmpty(validatingUri.Fragment))
{
// The endpoint URI MUST NOT include a fragment component.
}
else if (!Options.AllowInsecureHttp &&
String.Equals(validatingUri.Scheme, Uri.UriSchemeHttp, StringComparison.OrdinalIgnoreCase))
{
// The redirection endpoint SHOULD require the use of TLS
}
And you should pass "Authorize endpoint request missing required response_type parameter" and
"Authorize endpoint request contains unsupported response_type parameter"
Based on the other answers, I changed the Validation code in ApplicationOAuthProvider.cs to just ensure that the redirect uri is on the same domain like so:
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
if (context.ClientId == _publicClientId)
{
Uri expectedRootUri = new Uri(context.Request.Uri, "/");
if (context.RedirectUri.StartsWith(expectedRootUri.AbsoluteUri))
{
context.Validated();
}
}
return Task.FromResult<object>(null);
}
We recently replaced an old php website with an asp.net MVC website. In order to prevent 404 errors from the legacy urls in search engines, we setup a custom legacy route system via - http://www.mikesdotnetting.com/Article/108/Handling-Legacy-URLs-with-ASP.NET-MVC
The code works on my local machine, and it redirects to the correct route; however, the live server issues a 404. Bonus problem/clue, the 404 is not our custom 404 page but the iis 6 default page.
The code:
public class LegacyUrlRoute: RouteBase
{
// source: http://www.mikesdotnetting.com/Article/108/Handling-Legacy-URLs-with-ASP.NET-MVC
public override RouteData GetRouteData(HttpContextBase httpContext)
{
const string status = "301 Moved Permanently";
var request = httpContext.Request;
var response = httpContext.Response;
var legacyUrl = request.Url.ToString();
var newUrl = "";
if (legacyUrl.Contains(".php"))
{
newUrl = "/";
if (legacyUrl.Contains("support/mailfilter.php"))
newUrl = "/support/";
else if (legacyUrl.Contains("/support/default.php"))
newUrl = "/support/";
else if (legacyUrl.Contains("/business/default.php"))
newUrl = "/services/";
else if (legacyUrl.Contains("/residential/default.php"))
newUrl = "/services/";
else if (legacyUrl.Contains("/about/default.php"))
newUrl = "/home/about/";
else if (legacyUrl.Contains("/jobs.php"))
newUrl = "/jobs/";
else if (legacyUrl.Contains("/support/links.php"))
newUrl = "/support/";
else if (legacyUrl.Contains("/support/settings.php"))
newUrl = "/support/";
else if (legacyUrl.Contains("/default.php"))
newUrl = "/";
response.Status = status;
response.RedirectLocation = newUrl;
response.End();
}
return null;
}
public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
{
return null;
}
}
Note:
We suspect that the problem is with iis being unable to serve php pages, but I can't seem to find a setting in iis to fix the problem. It is as if the request never hits the Controller code, error or otherwise. All other pages/routing is working perfectly.
Found the answer.
The problem is due to the fact that iis never starts the asp.net service if the extension is .php.
The solution is to go to Properties>Home Directory>Configuration, find the .php extension, change the executable path to the asp.net path, and limit it to "GET,HEAD,POST,DEBUG" or whatever you prefer. I originally selected the "All Verbs" radio button, but that did not work.
Though the page did not specifically have the answer I came up with, this page did help.
I have a site that need to get some data from a different sit that is using asp.net MVC/
The data to get loaded is from these pages:
http://charity.hondaclassic.com/home/totaldonations
http://charity.hondaclassic.com/Home/CharityList
This should be a no brainer but for some reason I get an empty response, here is my JS:
<script>
jQuery.noConflict();
jQuery(document).ready(function($){
$('.totalDonations').load('http://charity.hondaclassic.com/home/totaldonations');
$('#charityList').load('http://charity.hondaclassic.com/home/CharityList');
});
</script>
in firebug I see the request is made and come back with a response of 200 OK but the response is empty, if you browse to these pages they work fine! What the heck?
Here are the controller actions from the MVC site:
public ActionResult TotalDonations() {
var total = "$" + repo.All<Customer>().Sum(x => x.AmountPaid).ToString();
return Content(total);
}
public ActionResult CharityList() {
var charities = repo.All<Company>();
return View(charities);
}
Someone please out what stupid little thing I am missing - this should have taken me 5 minutes and it's been hours!
The same origin policy prevents loading HTML from another web site via AJAX. The right way to do this would be to have the methods detect if the request is coming from AJAX and return JSONP instead.
public ActionResult TotalDonations( string callback )
{
var total = "$" + repo.All<Customer>().Sum(x => x.AmountPaid).ToString();
if (!string.IsNullOrEmpty(callback))
{
return Content( callback + "( { total: " + total + " } );" );
}
else
{
return Content(total);
}
}
...
$.getJSON('http://charity.hondaclassic.com/home/totaldonations?callback=?',
function(data) {
$('.totalDonations').html( data.total );
});
your totaldonations link is missing the o in total
> $('.totalDonations').load('http://charity.hondaclassic.com/home/ttaldonations');
should be
$('.totalDonations').load('http://charity.hondaclassic.com/home/totaldonations');
I ended up just doing it server side to avoid the same origin policy mentioned above:
Dim totalDonations As String
Dim charities As String
Using Client As New System.Net.WebClient()
totalDonations = Client.DownloadString("http://charity.hondaclassic.com/home/totaldonations")
charities = Client.DownloadString("http://charity.hondaclassic.com/home/CharityList")
End Using
worked like a charm.