I'm working on an MVC3 application and I'm using Elmah to handle my error logging. What I want in my application is to carry the Elmah Id onto the custom error page as I will provide a link which allows a user to specifically report it in the event that it is a repeat error (in their opinion).
Now, I've read similar questions on here and they suggest adding the following code (or similar) to the Global.asax.cs file:
void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
string sessionId = Session.SessionID;
Session["ElmahId_" + sessionId] = args.Entry.Id;
}
This is what I'm using at the moment, with the SessionID allowing for added flexibility in making the Session stored object unique. However, this may still cause issues if more than one error occurs at (virtually) the same time.
Instead, I decided to work on my own HandleErrorAttribute that looks something like this:
public class ElmahHandleErrorAttribute : FilterAttribute, IExceptionFilter
{
public void OnException(ExceptionContext filterContext)
{
if (filterContext == null)
throw new ArgumentNullException("filterContext");
if (filterContext.IsChildAction && (!filterContext.ExceptionHandled
&& filterContext.HttpContext.IsCustomErrorEnabled))
{
Elmah.ErrorSignal.FromCurrentContext().Raise(filterContext.Exception);
// get error id here
string errorId = null;
string areaName = (String)filterContext.RouteData.Values["area"];
string controllerName = (String)filterContext.RouteData.Values["controller"];
string actionName = (String)filterContext.RouteData.Values["action"];
var model = new ErrorDetail
{
Area = areaName,
Controller = controllerName,
Action = actionName,
ErrorId = errorId,
Exception = filterContext.Exception
};
ViewResult result = new ViewResult
{
ViewName = "Error",,
ViewData = new ViewDataDictionary<ErrorDetail>(model),
TempData = filterContext.Controller.TempData
};
filterContext.Result = result;
filterContext.ExceptionHandled = true;
filterContext.HttpContext.Response.Clear();
filterContext.HttpContext.Response.TrySkipIisCustomErrors = true;
}
}
}
where ErrorDetail is a custom model which just has the public properties that are being set here as strings. This data can then be shown in the model for admin's at a quick glance and the errorId can be used to create the 'Report Error' link.
So my question is does anyone know of a way of getting the Id after the line
Elmah.ErrorSignal.FromCurrentContext().Raise(filterContext.Exception)
without using the Logged event in the global.asax.cs?
Any thoughts are much appreciated.
After reading Dupin's comments it seems logical that it isn't quite possible. I tried digging around the Elmah source code and came up with a couple of alternatives that might be worth sharing.
The obvious alternative is stick with my original option of using the Logged event:
void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
string sessionId = Session.SessionID;
Session["ElmahId_" + sessionId] = args.Entry.Id;
}
For a more direct solution it is possible to manually log the error with the following:
string errorId = Elmah.ErrorLog.GetDefault(HttpContext.Current)
.Log(new Elmah.Error(filterContext.Exception));
However, using this approach won't hit your filters or mail module and so on.
After doing a bit of thinking and a little more searching, I came up with a new compromise. Still using the logged event but I've found a way to create a new unique key that can be passed to the view, by adding my own data to the exception.
string loggingKey = "ElmahId_" + Guid.NewGuid().ToString();
filterContext.Exception.Data.Add("LoggingKey", loggingKey);
This way I can pass the exception in my view model, which has this key value in the Data collection. The logged event would be changed to something like this:
void ErrorLog_Logged(object sender, ErrorLoggedEventArgs args)
{
string key = args.Entry.Error.Exception.Data["LoggingKey"].ToString();
Session[key] = args.Entry.Id;
}
Then in the view I get the key from the model to then pull the Id from the Session collection.
Maybe not very helpful but I suspect you can't get the error id at that point and you will need to use the logged event.
When you call
Elmah.ErrorSignal.FromCurrentContext().Raise(filterContext.Exception)
You're just raising the error. Depending on how you've configured ELMAH you might be logging the error or you might just send an email or a tweet.
There's no direct link between a raised error and an Id. That will only come with logging which, if you're feeling funny, you could be doing in multiple places and so creating multiple ids.
http://code.google.com/p/elmah/issues/detail?id=148#c3 is an identical request and a proposed patch on the Elmah project site
The solution above only works only if there is a Session object (website scenario). We needed it to work in an Azure WorkerRole, or a console / desktop app type setup. This solution will also work for web and save some session memory. There isn't a perfect solution, but one that worked for us to be able to log the error and retrieve the stored ID AND fire off an email is to:
Store the error using ErrorLog.Log(error) (see: Using ELMAH in a console application)
Raise the error skipping the logging (SQL or otherwise)
For the second part, we used the implementation of ElmahExtension given here: https://stackoverflow.com/a/2473580/476400
and REMOVED the following lines adding the logging:
(ErrorLog as IHttpModule).Init(httpApplication);
errorFilter.HookFiltering(ErrorLog); //removed!
The entire call from our client code looks like this:
ErrorLog errorLog = ErrorLog.GetDefault(null);
errorLog.ApplicationName = "YourAppName";
Error error = new Error(ex);
string errorResult = errorLog.Log(error);
Guid errorId = new Guid(errorResult);
ex.LogToElmah(); //this is just going to send the email
You might want to call that extention method something else, like RaiseToElmahNoStorage(), or something to indicate it is skipping the storage component.
Related
My understanding was OOTB, MVC will validate input to prevent XSS Attack and SQL Injection.
For example, In one of my app, the "a dangerous input has been detected" error will be received when I put in HTTP Get request. However, the post actions can let these values posted successfully through html input element without error. Even after I marked the controller action as [ValidateInput(true)]. How can I make them validate those post input?
Any advice will be appreciated!
Without seeing your GET handler, or what you're sending to it, it's tough to say why it behaves that way. However, OOTB MVC guards against SQL injection through the use of Entity Framework, and against XSS through ModelState validation.
Inside the body of your POST action that handles this forms submission you'll want to use code much like the following:
if (ModelState.IsValid)
{
//do the stuff I want to do when things are valid and free of XSS
}
else
{
//something went wrong. Probably shouldn't process this one. Have the user try again
}
Update: please disregard my filthy lies. ValidateInput(true) is not necessary because it is on by default. So, the only things I can think of would be that you have the AllowHtml attribute on your class or properties, or you are not posting back a model for modelBinding, and therefore input validation, to occur. At this point, you're probably going to need to put up some code for further help. There's too many unknowns right now.
I ran into a similar issue - we had JQuery using $.ajax to post JSON to the MVC action. The default model binder does not validate posted JSON allowing unsafe XSS to be posted against our action.
To solve this, I found the RequestValidator has a static method InvokeIsValidRequestString that allowed
public class ValidateJsonXssAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.HttpContext?.Request;
if (request != null && "application/json".Equals(request.ContentType, StringComparison.OrdinalIgnoreCase))
{
if (request.ContentLength > 0 && request.Form.Count == 0) //
{
if (request.InputStream.Position > 0)
request.InputStream.Position = 0; // InputStream has already been read once from "ProcessRequest"
using (var reader = new StreamReader(request.InputStream))
{
var postedContent = reader.ReadToEnd(); // Get posted JSON content
var isValid = RequestValidator.Current.InvokeIsValidRequestString(HttpContext.Current, postedContent,
RequestValidationSource.Form, "postedJson", out var failureIndex); // Invoke XSS validation
if (!isValid) // Not valid, so throw request validation exception
throw new HttpRequestValidationException("Potentially unsafe input detected");
}
}
}
}
}
Then, you can just decorate relevant MVC actions expecting JSON-posted data that might bypass the standard XSS prevention:
[HttpPost]
[ValidateJsonXss]
public ActionResult PublishRecord(RecordViewModel vm) { ... }
You can see other options for customizing request validation with OWASP .NET recommendations by extending the RequestValidator object, which exposes the string validation done by the ValidateInput automatically utilized by MVC for other scenarios of query string, form collection, and cookie values.
For more info: https://www.owasp.org/index.php/ASP.NET_Request_Validation
I have a link on a grid in my AdminUsers view
grid.Column(header: "", format: (item) => (condition ? Html.ActionLink("Impersonate", "Impersonate", "Admin", new { id = item.username }, null) : Html.Label("Impersonate"), style: "webgrid-column-link"),
In the controller, I have
public ActionResult Impersonate(string id)
{
string result = ORCA.utilities.users.setImpersonation(id);
if(result == "nocommonfields")
return RedirectToAction("AdminUsers", "Admin");
else
return RedirectToAction("terms_of_use", "Forms");
}
How can send an error message to display when I return to the AdminUsers page?
You may use TempData
if(result == "nocommonfields")
{
TempData["ErrorMessage"]="This is the message";
return RedirectToAction("AdminUsers", "Admin");
}
and in your AdminUsers action, you can read it
public ActionResult AdminUsers()
{
var errMsg=TempData["ErrorMessage"] as string;
//check errMsg value do whatever you want now as needed
}
Remember, TempData has very short-life span. Session is the backup storage behind temp data.
Alternatively, You may also consider sending a flag in your querystring and read it in your next action method and decide what error message to show.
The TempData controller property can be used to achieve this kind of functionality. Its main drawback in my opinion is that it uses the session storage in to store its contents. This means that you'll have extra work getting it to function on a web farm, or that you need to turn on sessions in the first place.
The good thing about TempData is that is exactly does what you want. Its a string based dictionary and you can put anything in it and by default get it out only once. So before calling RedirectToAction() you set your message. On the next request you check for messages and display them. By retrieving the messages they are automatically deleted at the end of the request.
As an alternative you could use cookies for transporting the message between the two requests. Essentially you could either roll your own solution, or implement a custom ITempDataProvider which transports the contents of TempData via cookies. Note that you need to properly secure cookies. MachineKey.Protect() can help you if you are rolling your own.
I was facing the same problem you did and created a solution for it called FlashMessage. Perhaps this could save you some work. It's available on NuGet as well. Usage is simple: you simply queue a message before you call RedirectToAction() as follows:
if(result == "nocommonfields")
{
FlashMessage.Warning("Your error message");
return RedirectToAction("AdminUsers", "Admin");
}
In your view you include the following statement to render any previously queued messages:
#Html.RenderFlashMessages()
I am completely baffled by this.
I have a public method on my controller which works on my development machine. But when I deploy the app I get an error message saying the method is not found;
[HttpGet]
[Authorize(Roles = "Administrator, AdminIT, ManagerIT")]
public ActionResult ListExistingIT(GridSortOptions sort, int? page)
{
if (Request.QueryString["lastPersonMessage"] == null)
ViewData["LastPersonMessage"] = string.Empty;
else
ViewData["LastPersonMessage"] = Request.QueryString["lastPersonMessage"];
EmployeeListViewModel elvm = new EmployeeListViewModel();
elvm.EmployeeList = EmployeeExtended.GetITEmployees();
if (sort.Column != null)
{
elvm.EmployeeList = elvm.EmployeeList.OrderBy(sort.Column, sort.Direction);
}
elvm.EmployeeList = elvm.EmployeeList.AsPagination(page ?? 1, Utility.GetPageLength());
ViewData["sort"] = sort;
return View(elvm);
}
The error message is; System.Web.HttpException: A public action method 'ListExistingIT' was not found on controller 'SHP.Controllers.EmployeeController'.
Now you might think that IIS is not picking up the latest deployment. However I make a change elsewhere and deploy it, and that works. I also restart IIS as well.
I cannot imagine how this happens, or how to detect where the error could be.
There is plenty of discussion on a similar (the same?) issue here on SO:Intermittent asp.net mvc exception: “A public action method ABC could not be found on controller XYZ.”
I can think of 3 different possibilities:
A conflict with the HttpVerb
causing the method not to be found.
A conflict with the filters applied
causing the method to be avoided.
A routing issue, but this one is
probably the last possibility. You
may want to try testing with the
RouteDebugger and see what that
shows you.
I've implemented a solution which involves Rhino.Security to manage user/roles/permissions.
Since I want to check if a user is authorized to access a controller action, I've implemented a custom action filter:
public class AuthorizationAttribute : ActionFilterAttribute
{
CustomPrincipal currentPrincipal = (CustomPrincipal)filterContext.HttpContext.User;
var actionName = filterContext.ActionDescriptor.ActionName;
var controllerName = filterContext.Controller.GetType().Name;
var operation = string.Format("/{0}/{1}", controllerName, actionName);
if (!securityService.CheckAuthorizationOnOperation(currentPrincipal.Code, operation))
{
filterContext.Controller.TempData["ErrorMessage"] = string.Format("You are not authorized to perform operation: {0}", operation);
filterContext.Result = new HttpUnauthorizedResult();
}
}
CheckAuthorizationOnOperation calls Rhino.Security to check if the user is allowed to the operation specified:
AuthorizationService.IsAllowed(user, operation);
Everything works properly but I've noticed that the second-level cache is never hit when the query called by IsAllowed is executed.
I've investigated and I've seen that the framework (Rhino.Security) uses a DetachedCriteria. These are the 2 procedures called:
public Permission[] GetGlobalPermissionsFor(IUser user, string operationName)
{
string[] operationNames = Strings.GetHierarchicalOperationNames(operationName);
DetachedCriteria criteria = DetachedCriteria.For<Permission>()
.Add(Expression.Eq("User", user)
|| Subqueries.PropertyIn("UsersGroup.Id",
SecurityCriterions.AllGroups(user).SetProjection(Projections.Id())))
.Add(Expression.IsNull("EntitiesGroup"))
.Add(Expression.IsNull("EntitySecurityKey"))
.CreateAlias("Operation", "op")
.Add(Expression.In("op.Name", operationNames));
return FindResults(criteria);
}
private Permission[] FindResults(DetachedCriteria criteria)
{
ICollection<Permission> permissions = criteria.GetExecutableCriteria(session)
.AddOrder(Order.Desc("Level"))
.AddOrder(Order.Asc("Allow"))
.SetCacheable(true)
.List<Permission>();
return permissions.ToArray();
}
As you can see FindResults uses SetCacheable.
Everytime I refresh a page my action filter executes the procedures and the query is executed again, ignoring the cache (second-level).
Since I use extensively the cache and all the other calls work properly I would like to understand why this one doesn't work as expected.
Doing some research I've noticed that the second-level cache is used only if I call the function twice:
SecurityService.CheckAuthorizationOnOperation(currentPrincipal.Code, "/Users/Edit");
SecurityService.CheckAuthorizationOnOperation(currentPrincipal.Code, "/Users/Edit");
It seems that the cache for this particular situation only works if I am using the same session (nHibernate).
Is there anybody who can try to help me to figure out what's happening?
UPDATE:
Make sure you are doing the work inside a transaction
Verify that the Permission entity is cached too
There's an issue with this framework.
I opened a question on Google Groups, everyone knows about it, but it seems that the framework has been forgotten.
I have a custom attribute that checks conditions and redirects the user to parts of the application as is necessary per business requirements. The code below is typical:
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
// ...
if (condition)
{
RouteValueDictionary redirectTargetDictionary = new RouteValueDictionary();
redirectTargetDictionary.Add("action", "MyActionName");
redirectTargetDictionary.Add("controller", "MyControllerName");
filterContext.Result = new RedirectToRouteResult(redirectTargetDictionary);
}
// ...
base.OnActionExecuting(filterContext);
}
I was just asked to allow the user to choose a default page that they arrive at upon logging in. Upon adding this feature, I noticed that the user can get some unusual behavior if there is no action/controller corresponding to the user's default page (i.e. if the application were modified). I'm currently using something like the code below but I'm thinking about going to explicit actions/controllers.
else if (condition)
{
var path = "~/MyControllerName/MyActionName";
filterContext.Result = new RedirectResult(path);
}
How do I check the validity of the result before I assign it to filterContext.Result? I want to be sure it corresponds to a working part of my application before I redirect - otherwise I won't assign it to filterContext.Result.
I don't have a finished answer, but a start would be to go to the RouteTable, get the collection, call GetRouteData with a custom implementation of HttpContextBase to get the RouteData. When done, if not null, check if the Handler is an MvcRouteHandler.
When you've got so far, check out this answer :)