I have a web application composed by frontend code that invokes the same api implemented by two backend modules. This api returns a url in a JSON object. The backend modules are both written with spring mvc but in different versions.
The url-building is the same and it is something like this:
#GetMapping(path = "/app1/menu", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public JsonObject getMenu(HttpServletRequest req) throws IOException {
JsonObject menu = new JsonObject();
menu.addProperty("href", ServletUriComponentsBuilder.fromRequest(req)
.replacePath(req.getContextPath())
.path("/index.html")
.toUriString());
return menu;
}
As you can see this code simply adds a constant to the incoming request and returns it.
The first app uses spring mvc 4 (4.3.5.RELEASE precisely).
The second module uses the 5.1.4.RELEASE version.
When all these apps are deployed on a load balanced server (2 tomcat instance with a load balancer upfront) and https the problem shows up.
Say that the request url is, for app1, something like this:
https://example.com/context/app1/menu
The app1 returns correctly
https://example.com/context/index.html
For the app2 the request issued by the frontend is
https://example.com/context/app2/menu
And the answer is
http://example.com/context/another_index.html
So it looses the https scheme
It seems that the ServletUriComponentsBuilder.fromRequest has changed behaviour?
I have taken a (quick I admit) look at the commits in the git repo but haven't
found anything ....
I have a single-page app (user loads a bunch of HTML/JS and then makes AJAX requests without another call to MVC - only via WebAPI). In WebAPI I have the following:
public sealed class WebApiValidateAntiForgeryTokenAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(
System.Web.Http.Controllers.HttpActionContext actionContext)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (actionContext.Request.Method.Method == "POST")
{
string requestUri = actionContext.Request.RequestUri.AbsoluteUri.ToLower();
if (uriExclusions.All(s => !requestUri.Contains(s, StringComparison.OrdinalIgnoreCase))) // place some exclusions here if needed
{
HttpRequestHeaders headers = actionContext.Request.Headers;
CookieState tokenCookie = headers
.GetCookies()
.Select(c => c[AntiForgeryConfig.CookieName]) // __RequestVerificationToken
.FirstOrDefault();
string tokenHeader = string.Empty;
if (headers.Contains("X-XSRF-Token"))
{
tokenHeader = headers.GetValues("X-XSRF-Token").FirstOrDefault();
}
AntiForgery.Validate(!string.IsNullOrEmpty(tokenCookie?.Value) ? tokenCookie.Value : null, tokenHeader);
}
}
base.OnActionExecuting(actionContext); // this is where it throws
}
}
Registered in Global.asax:
private static void RegisterWebApiFilters(HttpFilterCollection filters)
{
filters.Add(new WebApiValidateAntiForgeryTokenAttribute());
filters.Add(new AddCustomHeaderFilter());
}
Occasionally, I see the The anti-forgery cookie token and form field token do not match error in my logs. When this is happening, both tokenCookie.value and tokenHeader are not null.
Clientside, all of my AJAX requests use the following:
beforeSend: function (request) {
request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
},
With Razor generating the token once on my SPA page:
#Html.AntiForgeryToken()
I have my machine key set in Web.config.
What could be causing this?
Update
I just checked logs and I'm seeing this sometimes as well:
The provided anti-forgery token was meant for user "", but the current user is "someuser#domain.com". a few seconds ago
This occurs when a user refreshes their instance of the SPA while logged in. The SPA then drops them into the landing page instead of the inner page for some reason (User.Identity.IsAuthenticated is true) - then they can't log in because of this error. Refreshing pulls them back inside. Not sure what this means, but I figured more info can't hurt.
Appendix
https://security.stackexchange.com/questions/167064/is-csrf-protection-useless-with-ajax/167076#167076
My answer will recommend to not try to use CSRF protections based on tokens in AJAX calls, but rather to rely on the native CORS features of the web browser.
Basically, any AJAX call from the browser to the back-end server will check for the domain origin (aka the domain where the script was loaded from). If the domains match (JS hosting domain == target AJAX server domain) the AJAX calls performs fine, otherwise returns null.
If an attacker tries to host a malicious AJAX query on his own server it will fail if your back-end server has no CORS policy allowing him to do so (which is the case by default).
So, natively, CSRF protections are useless in AJAX calls, and you can lower your technical debt by simply not trying to handle that.
More info on CORS - Mozilla Foundation
Code example - use your console inspector!
<html>
<script>
function reqListener () {
console.log(this.responseText);
}
var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "http://www.reuters.com/");
oReq.send();
</script>
</html>
Run it and look at the Security error:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading
the remote resource at http://www.reuters.com/. (Reason: CORS header
‘Access-Control-Allow-Origin’ missing).
Mozilla is pretty clear regarding the Cross-site XMLHttpRequest implementation:
Modern browsers support cross-site requests by implementing the Web
Applications (WebApps) Working Group's Access Control for Cross-Site
Requests standard.
As long as the server is configured to allow requests from your web
application's origin, XMLHttpRequest will work. Otherwise, an
INVALID_ACCESS_ERR exception is thrown.
I try to give an answer the same, also if in the comments we exchange, yours it seems a not related scenario with mine..
A such type of issue can be due to the XMLHttpRequest.setRequestHeader() behaviour, because this function "combines" the values of an header that has been already assigned in the context of an http request, as stated by MDN and Whatwg:
If this method is called several times with the same header, the
values are merged into one single request header.
So, if we have a SPA for example that executes all ajax POSTs setting a given http header, in your case:
beforeSend: function (request) {
request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
}
the first ajax POST request sets a clear header ("X-XSRF-Token") and so, server side, you should have a "valid" header value to compare to.
But, in absence of a page refresh, or a new GET request, all subsequent ajax POSTs, as well as stated in the MDN and Whatwg documentation, will make a dirty assignment of the same header ("X-XSRF-Token"), because they combine the new values with the olds.
To avoid this issue, you could try to reset "X-XSRF-Token" value (but there isn't much documentation on that and it seems a not reliable solution...)
beforeSend: function (request) {
request.setRequestHeader("X-XSRF-Token", null); //depends on user agents..
//OR.. request.setRequestHeader("X-XSRF-Token", ''); //other user agents..
//OR.. request.setRequestHeader("X-XSRF-Token"); //other user agents..
request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
}
Other solutions can rely on some client-side state handing mechanism that you have to implement on your own, because it is not possible to get values or state access of the http request headers (only response headers can be accessed).
Update - revision of the following text:
So, if we have a SPA for example that executes all ajax POSTs recycling the XMLHttpRequest object for each calling and setting a given http header, in your case:
...
I've build a RESTful web service using NancyFX which I'm now trying to POST to from a separate domain. Of course when I do that, I see a failed OPTIONS message in the console because this is a Cross Site POST and I need to ensure that Nancy responds correctly to the OPTIONS message being sent by the browser. However, when I define a route in my Nancy module:
this.Options["/options/"] = _ => this.OptionsRequest();
private dynamic OptionsRequest()
{
return this.Response.AsJson(Request)
.WithHeader("Access-Control-Allow-Origin", "*")
.WithHeader("Access-Control-Allow-Methods", "POST")
.WithHeader("Access-Control-Allow-Headers", "Accept, Origin, Content-type");
}
The code never gets hit. I can set a breakpoint on the OptionsRequest() method in debugger and see that the code is never getting hit. However I can issue an OPTIONS request in postman, and the server returns a response (interestingly, it seems to return a response from all URIs, not just the /options/ route I've defined).
Is there a default Nancy OPTIONS behaviour I have to override in order to specify routes for OPTIONS, or is this something to do with the service being hosted in the Visual Studio Development Web Server (Casini)? I've tried everything I can think of and I'm still stumped as to why I can't define behaviour for this particular verb.
Turns out this was a bug in Nancy v0.17.1 (see https://github.com/NancyFx/Nancy/pull/1093). Upgrading to 0.18.0 fixed the issue and allowed my OPTIONS route to work correctly.
JS method
$.post('http://localhost:21067/HandlerServices/Product/ProductHandler.ashx', 'action=productlist', function (data) { console.log(data); console.log('hi') });
This ashx code is working but i recive nothing in response
This is ashx.cs code
context.Response.ContentType = "text/plain";
if (!string.IsNullOrEmpty(context.Request.QueryString["action"]))
{
string action = context.Request.QueryString["action"];
switch (action.ToLower())
{
case "productlist":
context.Response.Write("ersoy");
break;
}
}
I have query 1.9.0 version. In response tag not appeare anything.
Before i used it many times but now i cant understand where is the bug.
You are violating the same origin policy restriction that's built in browsers. Your ASP.NET MVC application containing this javascript file is hosted on http://localhost:2197 but you are attempting to perform an AJAX request to http://localhost:21067 which cannot work.
There are some workarounds such as using JSONP (works only with GET requests) or CORS (works only in modern browsers that support it). If for some reason you cannot use some of those techniques you could have a server side controller action inside your ASP.NET MVC application which will perform the actual call to the remote domain and act as a bridge between the 2. Then from your client script you will send the AJAX request to your own domain.
I was thinking how we can find out which server the page was served from: I'd do so by doing something like put a hidden variable on the page which has the IP or server name from the server it got processed. But what do I do for asp.net ajax requests: those that happen as a partial postback? I'd have to put the hidden variable in the update panel, but what if there are many update panels in the page?
I checked out another SO post, but the solution was for iis 7. What is the equivalent for iis6? And how can we read the header? Where to look?
You can set IIS6 custom headers via IIS MMC by opening a site's properties then clicking on the HTTP Headers tab:
You can also use adsutil (found in c:\InetPub\AdminScripts):
cscript adsutil set w3svc/1/root/HttpCustomHeaders "X-Served-By:Server-001"
The command above will configure the HTTP Headers for the default website.
Be careful when using adsutil as this will overwrite any existing headers already configured.
To set multiple headers do:
cscript adsutil set w3svc/1/root/HttpCustomHeaders "X-Served-By:Server-001" "X-Powered-By:ASP.NET"
Update:
With regard to accessing the response headers on the client, if you're using an ASP.NET AJAX update panel then add this script to the end of your page:
<script type="text/javascript" language="javascript">
Sys.WebForms.PageRequestManager.getInstance().add_endRequest(endPageRequest);
function endPageRequest(sender, args) {
var allHeaders = args._response._xmlHttpRequest.getAllResponseHeaders();
var headers = allHeaders.split('\n');
// At this point you have a string array of response headers.
// Or you can get an individual header:
var header = args._response._xmlHttpRequest.getResponseHeader("MyHeader");
}
</script>
This will hook into the page request manager such that when the Ajax request completes you also get visibility of the underlying XMLHttpRequest object which has a copy of the response headers.
You can do something similar with jQuery:
$.ajax({
url: "/Home/HeadTest",
success: function (data, textStatus, xhr) {
var header = xhr.getResponseHeader("MyHeader");
}
});