Allow sanitization of dotnet interop exceptions (dotnet/extensions#1879)

Adds the ability to sanitize exceptions during JS to .NET interop calls by overriding OnDotNetInvocationException method and returning an object to be returned to JavaScript that describes the exception.\n\nCommit migrated from d7eab7c083
This commit is contained in:
Javier Calvarro Nelson 2019-07-02 12:40:50 +02:00 committed by GitHub
parent b39ec06467
commit 244ed54764
4 changed files with 67 additions and 8 deletions

View File

@ -61,6 +61,7 @@ namespace Microsoft.JSInterop
protected JSRuntimeBase() { }
protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson);
public System.Threading.Tasks.Task<T> InvokeAsync<T>(string identifier, params object[] args) { throw null; }
protected virtual object OnDotNetInvocationException(System.Exception exception, string assemblyName, string methodIdentifier) { throw null; }
}
}
namespace Microsoft.JSInterop.Internal

View File

@ -103,7 +103,7 @@ namespace Microsoft.JSInterop
else if (syncException != null)
{
// Threw synchronously, let's respond.
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException);
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, syncException, assemblyName, methodIdentifier);
}
else if (syncResult is Task task)
{
@ -114,16 +114,17 @@ namespace Microsoft.JSInterop
if (t.Exception != null)
{
var exception = t.Exception.GetBaseException();
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception));
jsRuntimeBaseInstance.EndInvokeDotNet(callId, false, ExceptionDispatchInfo.Capture(exception), assemblyName, methodIdentifier);
}
var result = TaskGenericsUtil.GetTaskResult(task);
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result);
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, result, assemblyName, methodIdentifier);
}, TaskScheduler.Current);
}
else
{
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult);
jsRuntimeBaseInstance.EndInvokeDotNet(callId, true, syncResult, assemblyName, methodIdentifier);
}
}

View File

@ -62,18 +62,31 @@ namespace Microsoft.JSInterop
/// <param name="argsJson">A JSON representation of the arguments.</param>
protected abstract void BeginInvokeJS(long asyncHandle, string identifier, string argsJson);
internal void EndInvokeDotNet(string callId, bool success, object resultOrException)
/// <summary>
/// Allows derived classes to configure the information about an exception in a JS interop call that gets sent to JavaScript.
/// </summary>
/// <remarks>
/// This callback can be used in remote JS interop scenarios to sanitize exceptions that happen on the server to avoid disclosing
/// sensitive information to remote browser clients.
/// </remarks>
/// <param name="exception">The exception that occurred.</param>
/// <param name="assemblyName">The assembly for the invoked .NET method.</param>
/// <param name="methodIdentifier">The identifier for the invoked .NET method.</param>
/// <returns>An object containing information about the exception.</returns>
protected virtual object OnDotNetInvocationException(Exception exception, string assemblyName, string methodIdentifier) => exception.ToString();
internal void EndInvokeDotNet(string callId, bool success, object resultOrException, string assemblyName, string methodIdentifier)
{
// For failures, the common case is to call EndInvokeDotNet with the Exception object.
// For these we'll serialize as something that's useful to receive on the JS side.
// If the value is not an Exception, we'll just rely on it being directly JSON-serializable.
if (!success && resultOrException is Exception)
if (!success && resultOrException is Exception ex)
{
resultOrException = resultOrException.ToString();
resultOrException = OnDotNetInvocationException(ex, assemblyName, methodIdentifier);
}
else if (!success && resultOrException is ExceptionDispatchInfo edi)
{
resultOrException = edi.SourceException.ToString();
resultOrException = OnDotNetInvocationException(edi.SourceException, assemblyName, methodIdentifier);
}
// We pass 0 as the async handle because we don't want the JS-side code to

View File

@ -168,6 +168,38 @@ namespace Microsoft.JSInterop.Tests
Assert.Same(obj3, runtime.ObjectRefManager.FindDotNetObject(4));
}
[Fact]
public void CanSanitizeDotNetInteropExceptions()
{
// Arrange
var expectedMessage = "An error ocurred while invoking '[Assembly]::Method'. Swapping to 'Development' environment will " +
"display more detailed information about the error that occurred.";
string GetMessage(string assembly, string method) => $"An error ocurred while invoking '[{assembly}]::{method}'. Swapping to 'Development' environment will " +
"display more detailed information about the error that occurred.";
var runtime = new TestJSRuntime()
{
OnDotNetException = (e, a, m) => new JSError { Message = GetMessage(a, m) }
};
var exception = new Exception("Some really sensitive data in here");
// Act
runtime.EndInvokeDotNet("0", false, exception, "Assembly", "Method");
// Assert
var call = runtime.BeginInvokeCalls.Single();
Assert.Equal(0, call.AsyncHandle);
Assert.Equal("DotNet.jsCallDispatcher.endInvokeDotNetFromJS", call.Identifier);
Assert.Equal($"[\"0\",false,{{\"message\":\"{expectedMessage.Replace("'", "\\u0027")}\"}}]", call.ArgsJson);
}
private class JSError
{
public string Message { get; set; }
}
class TestJSRuntime : JSRuntimeBase
{
public List<BeginInvokeAsyncArgs> BeginInvokeCalls = new List<BeginInvokeAsyncArgs>();
@ -179,6 +211,18 @@ namespace Microsoft.JSInterop.Tests
public string ArgsJson { get; set; }
}
public Func<Exception, string, string, object> OnDotNetException { get; set; }
protected override object OnDotNetInvocationException(Exception exception, string assemblyName, string methodName)
{
if (OnDotNetException != null)
{
return OnDotNetException(exception, assemblyName, methodName);
}
return base.OnDotNetInvocationException(exception, assemblyName, methodName);
}
protected override void BeginInvokeJS(long asyncHandle, string identifier, string argsJson)
{
BeginInvokeCalls.Add(new BeginInvokeAsyncArgs