Automates MVC and WebApi exception handling#847
Conversation
| @@ -18,7 +18,7 @@ public static HttpCookie UnvalidatedGetCookie(this HttpRequest httpRequest, stri | |||
| public static string UnvalidatedGetHeader(this HttpRequest httpRequest, string headerName) | |||
| { | |||
| string value = httpRequest.Unvalidated.Headers[headerName]; | |||
There was a problem hiding this comment.
we should always expect a header value to be null
EnforceMaxLength has Debug.Assert that value is not null preventing to run tests locally in debug.
Instead, I can remove Debug.Assert from the EnforceMaxLength, however I don;t know what was the motivation putting it there in the first place.
@MS-TimothyMothra,
It seems you've done this change. could you please advise what is the best approach here?
There was a problem hiding this comment.
If it's causing problems, please remove it.
I added it because it seemed like it was the convention.
I have a backlog item to remove it myself and create actual unit tests for that Common project.
There was a problem hiding this comment.
will do.
I also wonder what is the need for enforcing max length on the incoming side? If request made it to the service, why would we trim it? Where 1024 came from?
There was a problem hiding this comment.
This risk was identified from SDL review.
Request headers are tracked in a dictionary and an attacker could spam an application with very large header values and force an out of memory exception.
Sergey gave me the 1024 value. That should be exaggeratedly larger than we need, but still small enough to protect ourselves from an attack.
| { | ||
| if (disposing) | ||
| { | ||
| this.isInitialized = false; |
There was a problem hiding this comment.
Why reset this? Do you want to allow re-initialization after Dispose? (Perhaps Initialize should check disposed and throw an ObjectDisposedException
There was a problem hiding this comment.
it's for testing only - as we need to initialize module more than once in the same process.
There was a problem hiding this comment.
It would be better to expose internal classes via InternalVisibleTo than complicating the public API
There was a problem hiding this comment.
while I can test underlying ExceptionHandlersInjector, testing it does not guarantee that it's ever called. So, in any case, I would need Dispose/clear logic on the ExceptionTrackingTelemetryModule to verify it calls ExceptionHandlersInjector.
I can substitute public dispose with internal isInitialized field.
I believe almost all modules are disposables and it fits into the model better than internal fields accessible by tests.
Thoughts?
| if (this.EnableMvcAndWebApiExceptionAutoTracking) | ||
| { | ||
| if (!this.isInitialized) | ||
| { |
There was a problem hiding this comment.
some other telemetry modules have this logic to ensure they are initialized just once as it seems it's not guaranteed by the base SDK.
Even if the injection is attempted more than once, it could be a bit expensive to run it more than once anyway, so I'd prefer it to be done just once.
There was a problem hiding this comment.
Sure, but are we forgetting to set isInitialized to true somewhere?
| rootRequestId = StringUtilities.EnforceMaxLength(rootRequestId, InjectionGuardConstants.RequestHeaderMaxLength); | ||
| if (rootRequestId != null) | ||
| { | ||
| rootRequestId = StringUtilities.EnforceMaxLength(rootRequestId, InjectionGuardConstants.RequestHeaderMaxLength); |
There was a problem hiding this comment.
same as before:
we should always expect a header value to be null
EnforceMaxLength has Debug.Assert that value is not null preventing to run tests locally in debug.
Instead, I can remove Debug.Assert from the EnforceMaxLength, however I don;t know what was the motivation putting it there in the first place.
@MS-TimothyMothra,
It seems you've done this change. could you please advise what is the best approach here?
There was a problem hiding this comment.
Please remove the Debug.Assert. (see comment above)
| using Microsoft.ApplicationInsights.DataContracts; | ||
| using Microsoft.ApplicationInsights.Extensibility.Implementation.Tracing; | ||
|
|
||
| internal sealed class ExceptionHandlersInjector |
fbfa592 to
619cbbf
Compare
|
|
||
| ## Version 2.6.0-beta2 | ||
| - [Implement unhandled exception auto-tracking (500 requests) for MVC 5 and WebAPI 2 applications] (https://github.com/Microsoft/ApplicationInsights-dotnet-server/pull/847) | ||
| - [Added a max length restriction to values passed in through requests.](https://github.com/Microsoft/ApplicationInsights-dotnet-server/pull/810) |
There was a problem hiding this comment.
Please add your comment to "2.6.0-beta3". Beta 2 is being released this week. I can explain offline.
| [Event(43, | ||
| Keywords = Keywords.Diagnostics, | ||
| Message = "{0} Injection failed. Error message: {1}", | ||
| Level = EventLevel.Verbose)] |
There was a problem hiding this comment.
I'd suggest informational or warning. It may help diagnose issues without going verbose
| // telemetryClient.TrackException(new ExceptionTelemetry(context.Exception)); | ||
| // base.OnLog(context); | ||
| // } | ||
| // public override void OnLog(ExceptionLoggerContext context) |
There was a problem hiding this comment.
so IL injection is better than reading load pre-compiled assembly from resources because we need to support multiple versions of MVC and will need binding redirect?
There was a problem hiding this comment.
Precompiled assemble must still be compiled with some specific version on MVC/WebApi.
To avoid binding redirects and assembly resolution issues, we considered an option here to use AppDomain.CurrentDomain.AssemblyResolve handler to manually resolve assembly versions when it's not possible automatically.
we preferred IL injection approach because
- such event fires for dlls MVC/WebApi depend on. And while public API surface is stable, particular dll dependencies might change: i.e. new dependency is introduced in MVC v6.
- it is more invasive and may potentially conflict with other components doing the same.
- while IL injection is harder to maintain, the thing we are doing here is small and simple and does not seem to be changed in future.
If we'd need to write more complicated logic, i'd say we should change it AppDomain.CurrentDomain.AssemblyResolve approach.
There was a problem hiding this comment.
Before this moment my definition of "small and simple" was a bit different =)
There was a problem hiding this comment.
:) I mean the code we are emitting is small and simple - just a few lines of code. I would not try to emit AppInisghts SDK
SergeyKanzhelev
left a comment
There was a problem hiding this comment.
See comments. Look OK overall
| } | ||
|
|
||
| public static HttpContext GetFakeHttpContext(IDictionary<string, string> headers = null, Func<string> remoteAddr = null) | ||
| public static HttpContext GetFakeHttpContext(IDictionary<string, string> headers = null, Func<string> remoteAddr = null, bool isCustomErrorEnabled = false) |
There was a problem hiding this comment.
isCustomErrorEnabled parameter does not seem to be used within the method - is it only required to satisfy the method signature as it's called out of our control somewhere?
There was a problem hiding this comment.
nice catch, parameter should be removed
| var exceptionTelemetryCtor = GetConstructorOrFail(typeof(ExceptionTelemetry), new[] { typeof(Exception) }); | ||
|
|
||
| var assemblyName = new AssemblyName(AssemblyName); | ||
| var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); |
There was a problem hiding this comment.
Does app need to run in full trust for those calls, do we need any SecurityPermissionFlags?
There was a problem hiding this comment.
excellent question!
-
I followed this guide to ensure this code requires only
SecurityPermissionFlags.Execute(the minimum possible).
So the answer is no, we don't need any flags. -
I've been trying to run any ASP.NET 5 app (empty one, no custom code or AI involved at all) with the trust level less than Full with no luck - it fails before any user code is executed. So I assume ASP.NET no longer supports other modes than Fill trust. My security and trust related knowledge in .NET is quite limited, so I can be wrong here.
Do you know something about it?
|
What will happen to customers who followed instructions? Will we collect exceptions twice? And if app catches and traces exceptions but doesn't rethrow them (responds with 500 directly) then this approach will not catch exceptions, correct? |
Yes, exceptions will be tracked twice.
Right, if app constructs 500 response itself, there may be no exception at all or it could be handled in the controller. In such case, we won't track it. It is the best practice to respond with 5XX for handled internal server errors and helps devops to classify known issues and distinguish them from unknown (500). We can just hope that if they manually catch exceptions they also manually track them. I'll update the article if you are ok with above. |
|
@zakimaksyutov please see my comments above |
|
@zakimaksyutov @SergeyKanzhelev @MS-TimothyMothra If there are no more concerns, can we merge it? |
|
No concerns. You can merge. |
This change automates MVC error handler and WebApi exception logger injection.
Now, we ask customers to do it manually as per this article: https://docs.microsoft.com/en-us/azure/application-insights/app-insights-asp-net-exceptions. VS 'Add AplicationInsights' wizard also adds MVC filter.
It allows tracking semi-unhandled exceptions - for requests resulted in 500 in the typical and default production case when custom errors handling was enabled.
This helps to onboard SnapshotDebugger customers ensuring important exceptions are tracked and in general, improves error detection for all AppInsights ASP.NET customers hosting their apps on IIS>
This change supports only WebAPI 2 and MVC4 (Microsoft.AspNet.Mvc/WebApi nuget version 5).
Earlier versions are not supported since ASP.NET nuget v5 was released in 2013 and is first to target .NET 4.5. (ASP.NET v4 targets .NET 4.0)
This change is done through the reflection-based dynamic code generation to prevent AI Web SDK dependency on MVC and WebApi dlls. If such dependency is introduced, it creates difficulties with binding redirects that are unresolvable with SDK light-up.
This reflection-based solution implements fail-fast approach - if some type is not resolved, it silently stops preventing further injection.
All type generation and injection are done at app startup (when ExceptionTrackingTelemetryModule is initialized) therefore performance impact on requests after service warms up remains intact.
Generation and injection time remains within reasonable few milliseconds range.
For significant contributions please make sure you have completed the following items:
Changes in public surface reviewed
the new property is introduced:
ExceptionTrackingTelemetryModule EnableMvcAndWebApiExceptionAutoTrackingto disable injection through the configDesign discussion issue - discussed over emails/meetings with @SergeyKanzhelev and @zakimaksyutov
CHANGELOG.md updated with one line description of the fix, and a link to the original issue.