How to secure ajax handlers? - asp.net

I am using post like this in http://www.mywebsite.com/hello.aspx page:
$.post("handler.ashx", {}, function (x) { alert(x); });
How to check the address from which the handler is running?
public void ProcessRequest (HttpContext context)
{
// check if request is from http://mywebsite/hello.aspx
context.Response.ContentType = "text/plain";
context.Response.Write("test");
}
or... how to disable request handler from different domain?

You can use the UrlReferrer to check if the call is comming from your site. One very simple working example:
if( !context.Request.UrlReferrer.Contains("site.com/")) )
{
context.Response.End();
return;
}
In some rare cases that users overwrite the Referrer, this fails.

Related

Check if a redirect then change the query string

I would like to remove the query parameter 'mobile' from all requests that are redirects. The page Redirect.aspx redirects a visitor to Default.aspx?mobile=1. And when a visitor browses to Redirect,aspx, then eventually he should be led to the Default.aspx without a parameter in the address bar.
Steps I took:
So, I have to remove the query parameter "mobile" from a query string if the current request is a redirect. And here is the problem: I'm checking if status code is 3xx and query has 'mobile' parameter, but this condition never equals true.
Redirect.aspx:
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
Context.Response.Redirect("Default.aspx?mobile=1");
}
RemoveParamModule:
public class RemoveParamModule : IHttpModule
{
public void Init(HttpApplication context)
{
context.EndRequest += RewriteHandler;
}
private void RewriteHandler(object sender, EventArgs eventArgs)
{
var context = (HttpApplication)sender;
var statusCode = context.Response.StatusCode;
if (statusCode.IsInRange(300, 399) && context.Request.QueryString["mobile"] != null)
{
DeleteMobileParameter(context.Request.QueryString);
context.Response.Redirect(context.Request.Path, true);
}
}
private static void DeleteMobileParameter(NameValueCollection collection)
{
var readOnlyProperty = collection.GetType().GetProperty("IsReadOnly", BindingFlags.Instance | BindingFlags.NonPublic);
readOnlyProperty.SetValue(collection, false, null);
collection.Remove("mobile");
readOnlyProperty.SetValue(collection, true, null);
}
public void Dispose()
{
}
}
Why do the requests in the module either have statusCode=302 or the parameter 'mobile' but never both at the same time? And how can I remove the parameter 'mobile' for the redirects?
Response.Redirect creates response from server for previously requested URL. Then, client browser receives this response and just GETs new URL, which server will process with usual 200 result.
So basically:
Request: GET Response.aspx
Response: 302 Default.aspx?mobile=1
Request: GET Default.aspx?mobile=1
Response: 200 <body>
So if I correctly understood your needs - you shouldn't parse mobile from Request URL, but analyze you Response instead.
Also Response.Redirect might throw ThreadAbortException so be careful with several Redirects in the same pipeline.

ASP.NET - Sign out user on browser tab closed

i need to sign out an authenticated user in ASP.NET reliably when a browser tab gets closed. What is the recommended solution?
Thanks
Typically you would do your sign-out logic when the session has ended. But if you had to detect when page is closed, use this:
<body onunload="performMyLogoutLogic();">
...
...
</body>
You could use a generic handler to kill session and call this beforeunload like this:
function CloseSession( )
{
location.href = 'KillSession.ashx?task=1';
}
window.onbeforeunload = CloseSession;
And in your KillSession.ashx do this
public void ProcessRequest(HttpContext context)
{
if(!String.IsNullOrWhiteSpace(Request.QueryString["task"].toString()))
{
if(Request.QueryString["task"].toString()=="1")
{
Session["User"]==null;
context.Response.ContentType = "text/plain";
context.Response.Write("Good Bye!");
}
}
}

How to prevent open redirection attacks?

what is the best approach to prevent open redirection attacks.Currently i am developing asp.net website.I want to make sure not to redirect the users to external links up on successful login?
Edit: Is it possible implement the solution without changing the existing code?
I'm assuming you're using the login control.
You should hook-up a check that the ReturnUrl parameter is a local url (and not one pointing to a different domain). The loggedin event would be a good place to do something like this:
void OnLoggedIn(object sender, EventArgs e)
{
string returnto = Request.QueryString["ReturnUrl"];
if (returnto != "" and isLocalUrl(returnto)) Response.Redirect(returnto);
}
where you can use the definition of IsLocalUrl given here
private bool IsLocalUrl(string url)
{
if (string.IsNullOrEmpty(url))
{
return false;
}
Uri absoluteUri;
if (Uri.TryCreate(url, UriKind.Absolute, out absoluteUri))
{
return String.Equals(this.Request.Url.Host, absoluteUri.Host,
StringComparison.OrdinalIgnoreCase);
}
else
{
bool isLocal = !url.StartsWith("http:", StringComparison.OrdinalIgnoreCase)
&& !url.StartsWith("https:", StringComparison.OrdinalIgnoreCase)
&& Uri.IsWellFormedUriString(url, UriKind.Relative);
return isLocal;
}
}

Forms authentication: disable redirect to the login page

I have an application that uses ASP.NET Forms Authentication. For the most part, it's working great, but I'm trying to add support for a simple API via an .ashx file. I want the ashx file to have optional authentication (i.e. if you don't supply an Authentication header, then it just works anonymously). But, depending on what you do, I want to require authentication under certain conditions.
I thought it would be a simple matter of responding with status code 401 if the required authentication was not supplied, but it seems like the Forms Authentcation module is intercepting that and responding with a redirect to the login page instead. What I mean is, if my ProcessRequest method looks like this:
public void ProcessRequest(HttpContext context)
{
Response.StatusCode = 401;
Response.StatusDescription = "Authentication required";
}
Then instead of getting a 401 error code on the client, like I expect, I'm actually getting a 302 redirect to the login page.
For nornal HTTP traffic, I can see how that would be useful, but for my API page, I want the 401 to go through unmodified so that the client-side caller can respond to it programmatically instead.
Is there any way to do that?
ASP.NET 4.5 added the Boolean HttpResponse.SuppressFormsAuthenticationRedirect property.
public void ProcessRequest(HttpContext context)
{
Response.StatusCode = 401;
Response.StatusDescription = "Authentication required";
Response.SuppressFormsAuthenticationRedirect = true;
}
After a bit of investigation, it looks like the FormsAuthenticationModule adds a handler for the HttpApplicationContext.EndRequest event. In it's handler, it checks for a 401 status code and basically does a Response.Redirect(loginUrl) instead. As far as I can tell, there's no way to override this behaviour if want to use FormsAuthenticationModule.
The way I ended up getting around it was by disabling the FormsAuthenticationModule in the web.config like so:
<authentication mode="None" />
And then implementing the Application_AuthenticateEvent myself:
void Application_AuthenticateRequest(object sender, EventArgs e)
{
if (Context.User == null)
{
var oldTicket = ExtractTicketFromCookie(Context, FormsAuthentication.FormsCookieName);
if (oldTicket != null && !oldTicket.Expired)
{
var ticket = oldTicket;
if (FormsAuthentication.SlidingExpiration)
{
ticket = FormsAuthentication.RenewTicketIfOld(oldTicket);
if (ticket == null)
return;
}
Context.User = new GenericPrincipal(new FormsIdentity(ticket), new string[0]);
if (ticket != oldTicket)
{
// update the cookie since we've refreshed the ticket
string cookieValue = FormsAuthentication.Encrypt(ticket);
var cookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName] ??
new HttpCookie(FormsAuthentication.FormsCookieName, cookieValue) { Path = ticket.CookiePath };
if (ticket.IsPersistent)
cookie.Expires = ticket.Expiration;
cookie.Value = cookieValue;
cookie.Secure = FormsAuthentication.RequireSSL;
cookie.HttpOnly = true;
if (FormsAuthentication.CookieDomain != null)
cookie.Domain = FormsAuthentication.CookieDomain;
Context.Response.Cookies.Remove(cookie.Name);
Context.Response.Cookies.Add(cookie);
}
}
}
}
private static FormsAuthenticationTicket ExtractTicketFromCookie(HttpContext context, string name)
{
FormsAuthenticationTicket ticket = null;
string encryptedTicket = null;
var cookie = context.Request.Cookies[name];
if (cookie != null)
{
encryptedTicket = cookie.Value;
}
if (!string.IsNullOrEmpty(encryptedTicket))
{
try
{
ticket = FormsAuthentication.Decrypt(encryptedTicket);
}
catch
{
context.Request.Cookies.Remove(name);
}
if (ticket != null && !ticket.Expired)
{
return ticket;
}
// if the ticket is expired then remove it
context.Request.Cookies.Remove(name);
return null;
}
}
It's actually slightly more complicated than that, but I basically got the code by looking at the implementation of FormsAuthenticationModule in reflector. My implementation is different to the built-in FormsAuthenticationModule in that it doesn't do anything if you respond with a 401 - no redirecting to the login page at all. I guess if that ever becomes a requirement, I can put an item in the context to disable the auto-redirect or something.
I'm not sure if this will work for everyone, but in IIS7 you can call Response.End() after you've set the status code and description. This way, that #&$^##*! FormsAuthenticationModule won't do a redirect.
public void ProcessRequest(HttpContext context) {
Response.StatusCode = 401;
Response.StatusDescription = "Authentication required";
Response.End();
}
To build on zacharydl's answer slightly, I used this to solve my woes. On every request, at the beginning, if it's AJAX, immediately suppress the behavior.
protected void Application_BeginRequest()
{
HttpRequestBase request = new HttpRequestWrapper(Context.Request);
if (request.IsAjaxRequest())
{
Context.Response.SuppressFormsAuthenticationRedirect = true;
}
}
I don't know how that Response.End() worked for you. I tried it with no joy, then looked at MSDN for Response.End(): 'stops execution of the page, and raises the EndRequest event'.
For what it's worth my hack was:
_response.StatusCode = 401;
_context.Items["401Override"] = true;
_response.End();
Then in Global.cs add an EndRequest handler (which will get called after Authentication HTTPModule):
protected void Application_EndRequest(object sender, EventArgs e)
{
if (HttpContext.Current.Items["401Override"] != null)
{
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.StatusCode = 401;
}
}
what you've found out is correct about the forms auth intercepting the 401 and doing a redirect but we also can do that to reverse that.
Basically what you need is an http module to intercept the 302 redirect to the login page and reverse it to a 401.
Steps on doing that is explained in here
The given link is about a WCF service but it is the same in all the forms auth scenarios.
As explained in the above link you need to clear the http headers as well but remember to put the cookie header back to the response if the original response (i.e. before intercepting) contained any cookies.
I know there is already an answer with tick but while trying to solve similar problem I found this (http://blog.inedo.com/2010/10/12/http-418-im-a-teapot-finally-a-%e2%80%9clegitimate%e2%80%9d-use/) as an alternative.
Basically you return your own HTTP status code (e.g. 418) in your code. In my case a WCF data service.
throw new DataServiceException(418, "401 Unauthorized");
Then use a HTTP module to handle it at the EndRequest event to rewrite the code back to 401.
HttpApplication app = (HttpApplication)sender;
if (app.Context.Response.StatusCode == 418)
{
app.Context.Response.StatusCode = 401;
}
The browser / client will receive the correct content and status code, it works great for me :)
If you are interested to learn more about HTTP status code 418 see this question & answer.
That's a known issue, and there's a NuGet Package for that and/or the source code available.
Funny hack if you use.NET Framework >= v4.0 but < v4.5. It uses reflection to set value of inaccessible SuppressFormsAuthenticationRedirect property:
// Set property to "true" using reflection
Response
.GetType()
.GetProperty("SuppressFormsAuthenticationRedirect")
.SetValue(Response, true, null);
You do not set the WWW-Authenticate header in the code you show, so the client cannot do HTTP authentication instead of forms authentication. If this is the case, you should use 403 instead of 401, which will not be intercepted by the FormsAuthenticaitonModule.
I had the problem that I wanted to avoid not only the redirect but also the forms authentication itself in order to make a web api work. Entries in web.config with a location tag for the api didn't help.
Thus I used SuppressFormAuthenticationRedirect and HttpContext.Current.SkipAuthorization to suppress the authentication in general.
In order to identify the sender I used e.g. the UserAgent in the Header but it is of course recommendable to implement further authentification steps, e.g. check against the sending IP or send another key with the request.
Below is inserted in the Global.asax.cs.
protected void Application_BeginRequest(object sender, EventArgs e)
{
if (HttpContext.Current.Request.UserAgent == "SECRET-AGENT")
{
AppLog.Log("Redirect suppressed");
HttpApplication context = (HttpApplication)sender;
context.Response.SuppressFormsAuthenticationRedirect = true;
HttpContext.Current.SkipAuthorization = true;
}
}
In order to redirect the user to Unauthorize Page rather than to the login page, the Hack is to implement Application_EndRequest in Global and check for Response Status Code 302, which is a temporary redirect from the current called to action.
protected void Application_EndRequest(object sender, EventArgs e)
{
if(HttpContext.Current.Response.StatusCode == 302 && User.Identity.IsAuthenticated)
{
HttpContext.Current.Response.Clear();
HttpContext.Current.Response.Redirect("/UnauthorizedPageUrl");
}
}
Look inside your Web.config file in configuration\authentication.
If there is a forms subelement there with a loginUrl attribute, remove it and try again.

How do you handle ajax requests when user is not authenticated?

How do you handle ajax requests when user is not authenticated?
Someone enters the page, leaves room for an hour, returns, adds comment on the page that goes throuh ajax using jQuery ($.post). Since he is not authenticated, method return RedirectToRoute result (redirects to login page). What do you do with it? How do you handle it on client side and how do you handle it in controller?
EDIT:
I wrote above answer a long time ago and now I believe that sending 403 is not proper way to go. 403 has slightly different meaning and it just shouldn't be used. This is corrected attribute using 401. It differs only with additional context.HttpContext.Response.End() in Http401Result and different HTTP code:
public class OptionalAuthorizeAttribute : AuthorizeAttribute
{
private class Http401Result : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
// Set the response code to 401.
context.HttpContext.Response.StatusCode = 401;
context.HttpContext.Response.Write(CTRes.AuthorizationLostPleaseLogOutAndLogInAgainToContinue);
context.HttpContext.Response.End();
}
}
private readonly bool _authorize;
public OptionalAuthorizeAttribute()
{
_authorize = true;
}
//OptionalAuthorize is turned on on base controller class, so it has to be turned off on some controller.
//That is why parameter is introduced.
public OptionalAuthorizeAttribute(bool authorize)
{
_authorize = authorize;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//When authorize parameter is set to false, not authorization should be performed.
if (!_authorize)
return true;
var result = base.AuthorizeCore(httpContext);
return result;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
//Ajax request doesn't return to login page, it just returns 401 error.
filterContext.Result = new Http401Result();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
OLD ANSWER:
While I like the ideas posted in other answers (which I had an idea about earlier), I needed code samples. Here they are:
Modified Authorize attribute:
public class OptionalAuthorizeAttribute : AuthorizeAttribute
{
private class Http403Result : ActionResult
{
public override void ExecuteResult(ControllerContext context)
{
// Set the response code to 403.
context.HttpContext.Response.StatusCode = 403;
context.HttpContext.Response.Write(CTRes.AuthorizationLostPleaseLogOutAndLogInAgainToContinue);
}
}
private readonly bool _authorize;
public OptionalAuthorizeAttribute()
{
_authorize = true;
}
//OptionalAuthorize is turned on on base controller class, so it has to be turned off on some controller.
//That is why parameter is introduced.
public OptionalAuthorizeAttribute(bool authorize)
{
_authorize = authorize;
}
protected override bool AuthorizeCore(HttpContextBase httpContext)
{
//When authorize parameter is set to false, not authorization should be performed.
if (!_authorize)
return true;
var result = base.AuthorizeCore(httpContext);
return result;
}
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.RequestContext.HttpContext.Request.IsAjaxRequest())
{
//Ajax request doesn't return to login page, it just returns 403 error.
filterContext.Result = new Http403Result();
}
else
base.HandleUnauthorizedRequest(filterContext);
}
}
HandleUnauthorizedRequest is overridden, so it returns Http403Result when using Ajax. Http403Result changes StatusCode to 403 and returns message to the user in response. There is some additional logic in attribute (authorize parameter), because I turn on [Authorize] in the base controller and disable it in some pages.
The other important part is global handling of this response on client side. This is what I placed in Site.Master:
<script type="text/javascript">
$(document).ready(
function() {
$("body").ajaxError(
function(e,request) {
if (request.status == 403) {
alert(request.responseText);
window.location = '/Logout';
}
}
);
}
);
</script>
I place a GLOBAL ajax error handler and when ever $.post fails with a 403 error, the response message is alerted and the user is redirected to logout page. Now I don't have to handle the error in every $.post request, because it is handled globally.
Why 403, and not 401? 401 is handled internally by MVC framework (that is why redirection to login page is done after failed authorization).
What do you think about it?
The idea I came up with when a coworker asked about how to handle it was this - make an AuthorizeAjax attribute. It can interrogate and verify that Request.IsAjaxRequest() and, if the request isn't authenticated, return a specific JSON error object. It's possible you could simply override the default AuthorizeAttribute and have it call the base unless it's an unauthorized AJAX request so you don't have to worry about whether to tag controller actions with [Authorize] or [AuthorizeAjax].
On the client-side, all your pages would have to be equipped to deal with the returned error, but that logic can likely be shared.
I would propose creating your own AuthorizeAttribute and if the request is an Ajax request, throw an HttpException(401/403). And also switch to use jQuery's Ajax Method instead.
Assuming you've implemented error pages and they return the correct status code, the error callback will be executed instead of the success callback. This will be happen because of the response code.
The simplest and cleanest solution I've found for this is to register a callback with the jQuery.ajaxSuccess() event and check for the "X-AspNetMvc-Version" response header.
Every jQuery Ajax request in my app is handled by Mvc so if the header is missing I know my request has been redirected to the login page, and I simply reload the page for a top-level redirect:
$(document).ajaxSuccess(function(event, XMLHttpRequest, ajaxOptions) {
// if request returns non MVC page reload because this means the user
// session has expired
var mvcHeaderName = "X-AspNetMvc-Version";
var mvcHeaderValue = XMLHttpRequest.getResponseHeader(mvcHeaderName);
if (!mvcHeaderValue) {
location.reload();
}
});
The page reload may cause some Javascript errors (depending on what you're doing with the Ajax response) but in most cases where debugging is off the user will never see these.
If you don't want to use the built-in header I'm sure you could easily add a custom one and follow the same pattern.
Here's a solution I use. It is dead simple, if a bit brute-force. I like it because I'm lazy and I don't want to think about special attributes on action methods and I don't want to write ajax error handlers if I don't have to (although there's no reason client script couldn't detect the 403 status code and do something user friendly).
Putting this in Global.axax detects any unauthenticated ajax request and simply returns 403, with no content. This prevents unauthenticated ajax calls getting redirected to the login form when forms authentication is in use.
protected void Application_AuthenticateRequest(object sender, EventArgs e)
{
// Prevent Ajax requests from being returned the login form when not authenticated
// (eg. after authentication timeout).
if ((Request.Headers["X-Requested-With"] != null && Request.Headers["X-Requested-With"] == "XMLHttpRequest")
||
(Request["X-Requested-With"] != null && Request["X-Requested-With"] == "XMLHttpRequest"))
{
if (!Request.IsAuthenticated)
{
Response.Clear();
Response.StatusCode = 403;
Response.Flush();
Response.End();
}
}
}
You can detect ajax request and send 401, and on client side you can even show an ajax dialog with login prompt, after which you can "continue" your failed ajax request and make your application work and user feel like session timeout never happened. See this answer for details.

Resources