I am designing a new website and I am considering using AJAX post requests for better user experience. Is using AJAX POST requests for changing server state an acceptable design practice? Are their any security concerns in using AJAX POST requests? Is it recommended to restrict the server state changes to HTTP POST only?
EDIT
I am using ASP.NET MVC web framework for implementation.
Post, Put, Patch and Delete (although the last one is barely used) are all request types that traditionally alter the server state.
In order to answer your question, it is important to consider which framework you are using, as each one might have different best practices.
From a technical point of view, they all do practically the same, they only have different semantic meanings and conventions attached to them. If you were to use Post for everything, I doubt that anybody would complain
Post back is traditional way to doing things on web application where whole page re-load on form submission. In this approach most of the codes runs at sever side.
AJAX is a modern way to building web application where most of the code runs at client side for better performance and user experience. Only required data post to server instead of posting whole page.
Post back & Ajax both create HTTP request so it is not right to say one is less secure than other. In both request attacker can inject script using cross-site scripting (XSS) or CSRF (Cross-site request forgery).
AJAX calls are itself protect CSRF using “Common Origin Policy” when CORS is disabled and JSONP requests are blocked. To prevent CSRF attack one step ahead, you can implement Anti Forgery token like in MVC framework. AJAX calls can be called from web application as well as from MVC.
In MVC, #html.antiforgerytoken() can be called on form load which store one key in hidden field and other key in cookie and using ValidateAntiForgeryToken filter, we can validate that CSRF token. The form token can be a problem for AJAX requests, because an AJAX request might send JSON data, not HTML form data. One solution is to send the tokens in a custom HTTP header.
Here is sample code snippet for more details…
Sample Server side Code to generate Anti forgery token.
/// <summary>
/// Get Anti Forgery token
/// </summary>
/// <returns></returns>
public static string GetAntiXsrfToken()
{
string cookieToken, formToken;
AntiForgery.GetTokens(null, out cookieToken, out formToken);
var responseCookie = new HttpCookie("__AntiXsrfToken")
{
HttpOnly = true,
Value = cookieToken
};
if (FormsAuthentication.RequireSSL && HttpContext.Current.Request.IsSecureConnection)
{
responseCookie.Secure = true;
}
HttpContext.Current.Response.Cookies.Set(responseCookie);
return formToken;
}
Sample Server side Code to validate Anti forgery token.
/// <summary>
/// Validate Anti Forgery token coming from secure cookie & request header
/// </summary>
static void ValidateAntiXsrfToken()
{
string tokenHeader, tokenCookie;
try
{
// get header token
tokenHeader = HttpContext.Current.Request.Headers.Get("__RequestVerificationToken");
// get cookie token
var requestCookie = HttpContext.Current.Request.Cookies["__AntiXsrfToken"];
tokenCookie = requestCookie.Value;
AntiForgery.Validate(tokenCookie, tokenHeader);
}
catch
{
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.StatusCode = 403;
HttpContext.Current.Response.End();
}
}
Sample code to get Anti forgery token (one part) and save into hidden field
<input name="__RequestVerificationToken" type="hidden" value="<%= CommonUtils.GetAntiXsrfToken() %>" />
Sample client side code to pass one part to Anti Forgery token into request header from hidden field and another part will go automatically from client cookie if request is generated from same origin.
function CallServer(baseUrl, methodName, MethodArgument, callback) {
$.ajax({
type: "POST",
url: baseUrl + methodName,
data: MethodArgument,
contentType: "application/json; charset=utf-8",
async: false,
dataType: "json",
headers: {'__RequestVerificationToken': $("input[name='__RequestVerificationToken']").val()
},
success: function (data) {
if (callback != undefined && typeof (callback) === "function") {
callback(data.d);
}
},
error: function (data) {
if (data.status == 401 || data.status == 403)
window.location.href = "../Common/accessdenied";
else if (data.status == 419) {
displayUserMessage(commonMessage.RE_SESSIONINFO_NOT_FOUND, true);
window.location.href = "../Common/logout";
}
else
displayUserMessage(commonMessage.SERVICE_NOT_RESPONDING, true);
}
});
}
Finally, Call ValidateAntiXsrfToken() function before processing the each AJAX request at server side.
You can find more details here…
Which one is better? Ajax post or page post[Controller httppost] when only one form is there in a page?
http://www.asp.net/web-api/overview/security/preventing-cross-site-request-forgery-csrf-attacks
https://www.owasp.org/index.php/Cross-Site_Request_Forgery_%28CSRF%29_Prevention_Cheat_Sheet
Related
I have this REST service on domainA:
#CrossOrigin(origins={"http://domainB"})
#RequestMapping(value="/csrf", method=RequestMethod.GET)
public #ResponseBody
CsrfToken getCsrfToken(HttpServletRequest request) {
CsrfToken token = (CsrfToken)request.getAttribute(CsrfToken.class.getName());
return token;
}
Then I want to obtain CSRF token from above service (by using javascript on domainB) and add it to a <form action="http://domainA> on domainB and send this form to domainA (it is a simple form that has a submit button).
The problem is I get HTTP Status 403 – Forbidden.
As the opposite: when I manually set the _csrf value (obtained manually in the other browser tab pointing to domainA/csrf) in the <form action="http://domainA> and submit it then it works.
The difference which I noticed is that when I manually refresh browser's tab domainA/csrf then I get constantly the same value (and this value works), but when the domainA/csrf is obtained by the javascript from the domainB it is each time different and when using it - it does not work.
Can anyone help?
domainA: www.fridayweekend.com/rest/csrf
domainB: www.friwee.com/register (hit F12 and observe what call to www.fridayweekend.com/rest/csrf returns....)
As #dur said - the problem was in the JavaScript code. I used:
$.getJSON(domainA/csrf, callback)
which was ending up each time with a new session and a new CSRF token for it.
The solution was to use cors_ajax_call function except $.getJSON, defined as below:
var cors_ajax_call = function(address, callback){
$.ajax({
url: address,
context: document.body,
xhrFields: {
withCredentials: true
}
}).success(callback);
}
Thank you for your input! Hope this help someone :)
I am working on a sample application using ASP.NET MVC and AngularJS.
In server side code , I have written a Action filter attribute , and in that I need to check whether the request is a normal request(Browser) or AJAX request.
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
if ( filterContext.HttpContext.Request.IsAjaxRequest())
{
}
}
The method mentioned in the above code snippet "IsAjaxRequest()" is not returning TRUE in case of AJAX request made using $http Angular service.
I observed that the request does not have X-Requested-With header , and even adding the header did not solve the request.
Note : This is NOT CORS call.
So My question.
How does filterContext.HttpContext.Request.IsAjaxRequest() decide whether the request is AJAX or not?
I can check the request header(whether it has a particular header or not) and decide whether the request is AJAX or not. Is it the right and only approach?
It decides by looking whether X-Requested-With header exists or not.
You can add X-Request-With header manually to $http service.
Individual request
$http.get('/controller/action', {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
For every request
app.config(['$httpProvider', function ($httpProvider) {
$httpProvider.defaults.headers.common["X-Requested-With"] = 'XMLHttpRequest';
}]);
You can see why it is missing from Angular
We have MVC 4 application which is hosted on Web Farm. Site has 5 applications hosted under one domain. These applications communicate with each other. We are implementing Cross Site Request Forgery for our application. We have added AntiForgeyToken(#Html.AntiForgeryToken()) on Layout page. When we try to post data actions across applications using Ajax request, we are facing below exception-
Exception:
The anti-forgery token could not be decrypted. If this application is hosted by a Web Farm or cluster, ensure that all machines are running the same version of ASP.NET Web Pages and that the configuration specifies explicit encryption and validation keys. AutoGenerate cannot be used in a cluster.
For Ajax request we have added “__RequestVerificationToken” value into prefilter as shown below-
Client side implementation:
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
if (options.type.toLowerCase() == "post") {
if ($('input[name="__RequestVerificationToken"]').length > 0)
jqXHR.setRequestHeader('__RequestVerificationToken',
$('input[name="__RequestVerificationToken"]').val());
}});
On Server side we validated this token as shown below-
string cookie = "";
Dictionary<string, object> cookieCollection = new Dictionary<string, object>();
foreach (var key in HttpContext.Current.Request.Cookies.AllKeys)
{
cookieCollection.Add(key, (HttpContext.Current.Request.Cookies[key]));
}
var res= cookieCollection.Where(x => x.Key.Contains("RequestVerificationToken")).First();
cookie = ((System.Web.HttpCookie)(res.Value)).Value;
string formToken = Convert.ToString(HttpContext.Current.Request.Headers["__RequestVerificationToken"]);
if(string.IsNullOrEmpty(formToken))
{
//To validate HTTP Post request
AntiForgery.Validate();
}
else
{
//To validate Ajax request
AntiForgery.Validate(cookie, formToken);
}
Other configurations which we have done are as below-
We have machine key in config which is same for all applications as well as on all web servers.
We have set AntiForgeryConfig.CookieName = "__RequestVerificationToken" + "_XYZ" cookie name across all applications which is mandatory to access token across applications.
Changes which we tried to resolve this issue-
We tried to post “__RequestVerificationToken” inside each ajax request’s data so that we can access it using Request.Form but no success on this.
We have verified that Content-Type header is appearing with each request.
Please suggest if you have any other way to implement CSRF functionality for Ajax POST requests across multiple applications.
The MVC4 SPA template has a ValidateHttpAntiForgeryTokenAttribute class with a ValidateRequestHeader function that parses out 2 halves of a RequestVerificationToken header constructed by the client when it assembles the data for an AJAX call. The client AJAX code gets its value from a field on the form that combines a form token and a cookie token. I feel like combining these into a single value loses sight of the purpose of an AntiForgery token; they were separate for a reason. Do we gain any security by using anti-forgery tokens in this way?
Server .vbhtml code (Razor-based):
Public Function GetAntiForgeryToken() As String
Dim cookieToken As String = String.Empty
Dim formToken As String = String.Empty
AntiForgery.GetTokens(Nothing, cookieToken, formToken)
Return cookieToken & ":" & formToken
End Function
...
#If User.Identity.IsAuthenticated Then
#<input id="antiForgeryToken" type="hidden" value="#GetAntiForgeryToken()" />
End If
Client AJAX code:
function ajaxRequest(type, url, data, dataType) { // Ajax helper
var options = {
dataType: dataType || "json",
contentType: "application/json",
cache: false,
type: type,
data: data ? ko.toJSON(data) : null
};
var antiForgeryToken = $("#antiForgeryToken").val();
if (antiForgeryToken) {
options.headers = {
'RequestVerificationToken': antiForgeryToken
}
}
return $.ajax(url, options);
}
Server validation code:
Private Sub ValidateRequestHeader(request As HttpRequestMessage)
Dim cookieToken As String = String.Empty
Dim formToken As String = String.Empty
Dim tokenHeaders As IEnumerable(Of String) = Nothing
If request.Headers.TryGetValues("RequestVerificationToken", tokenHeaders) Then
Dim tokenValue As String = tokenHeaders.FirstOrDefault()
If Not String.IsNullOrEmpty(tokenValue) Then
Dim tokens As String() = tokenValue.Split(":"c)
If tokens.Length = 2 Then
cookieToken = tokens(0).Trim()
formToken = tokens(1).Trim()
End If
End If
End If
AntiForgery.Validate(cookieToken, formToken)
End Sub
What prevents a client from picking any arbitrary pair of cookieToken and formToken that was used in the past, and submitting them together in an AJAX call to get it to go through? Isn't that what anti-forgery functions are supposed to prevent? Is this just a lot of stupid overhead that doesn't improve security, or is there a piece of it that I'm missing?
What prevents a client from picking any arbitrary pair of cookieToken and formToken that was used in the past, and submitting them together in an AJAX call to get it to go through? Isn't that what anti-forgery functions are supposed to prevent? Is this just a lot of stupid overhead that doesn't improve security, or is there a piece of it that I'm missing?
The anti-forgery token is not designed to prevent a Replay Attack. This is where old values are reused to create another request where the aim is to fool the target machine into accepting a valid instruction from the past.
The anti-forgery token is designed to prevent Cross Site Request Forgery attacks.
A simple example is as follows:
You're logged into bank.com on one tab.
You get an email to view a funny video by clicking on a link that redirects you to
evil.com
The web page on evil.com contains a hidden form that is submited by JavaScript to bank.com/make_money_transfer
As you are logged into bank.com and your cookies are sent by the browser, bank.com thinks that you made the request and initiates the money transfer without your knowledge, because from the server's point of view, all is well.
The token is designed to prevent this by having something included in the request payload that cannot be auto submitted by a domain that is not the current domain. Due to the Same Origin Policy another domain cannot access the token value and therefore cannot send a legitimate request via a hidden form, or by any other means. The token is unique per log in session so the attacker could not get a valid combination of token and cookie which can be sent to the server.
Looking at the source code for TokenValidator.cs (albeit C# instead of VB.NET) we can see that the ValidateTokens method checks that the username encoded in the token matches the one of the current HTTP request:
if (!String.Equals(fieldToken.Username, currentUsername, (useCaseSensitiveUsernameComparison) ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase))
{
throw HttpAntiForgeryException.CreateUsernameMismatchException(fieldToken.Username, currentUsername);
}
This is what will stop an attacker grabbing an old version of the form field value and submitting that in their CSRF attack - their encoded username will not match the logged in user of their victim.
I have designed a custom route which looks like below
"\client\{CLIENTCODE}\{Controller}\{View}\{id}"
other than this route I also have default MVC route intact.
The {CLIENTCODE} is 4 character length string in the URL,which will be used to detect a connection string and do operation on respective database.
I am facing two issues
All Ajax request take default route when I use AJAX URL as 'Controller\View'. How can I append {CLIENTCODE} to every AJAX request.
I am loosing {CLIENTCODE} from URL after the session expires and I am unable to get it in Global.ASAX.
If u need append this route to ajax request you need set the ajax url with your route.
$.ajax({
type: "POST",
url: '#Url.RouteUrl("routeName", new { code="code", controller="controller", action="action"})',
dataType: "html",
success: function (data) {
$("#product-attribute-values").append(data);
}
})
And what you mean 'loosing when session expired'? You can acces all route values with code like this in global.asax
protected void Application_BeginRequest()
{
string code = Request.RequestContext.RouteData.Values["code"].ToString();
}