Closed
Description
This is part of #10472
Clients shouldn't be able to make the process terminate, whether accidentally or deliberately.
Currently there may be cases where they can (e.g., with JSInterop, sending a call with an unknown object ID).
We need to check:
- All places where we receive input from the client. Ensure there's no way that accidentally or deliberately bad input can cause a global exception. It is fine if they can kill their own circuit (though should be logged), but it shouldn't be able to affect other circuits.
- JS interop inbound calls, starting from
ComponentHub.BeginInvokeDotNetFromJS
- Replies to outbound JS interop calls (sync and async)
- Event notifications (starting from
renderer.DispatchEventAsync
, treating input as untrusted)- OK, because any unhandled exception in
DispatchEventAsync
becomes a regular failed sync/async JS interop call, so this is scoped to the circuit
- OK, because any unhandled exception in
- Render batch ACKs, starting from
ComponentHub.OnRenderCompleted
- OK. If it's an unknown batch ID, we send an error to the circuit's exception handler. For arbitrary exceptions besides this (which may be impossible), it's a regular hub method exception, which SignalR handles by logging it and sending a redacted copy of the exception info to the client, and terminates that one connection/circuit.
- Action needed Clients can send an arbitrary
errorMessageOrNull
value, which will typically end up in server logs. So they might be able to mislead the developer into thinking that other errors are happening. Developers need to understand somehow that these error messages are untrusted input and should be treated with suspicion. Clarify that ComponentHub.OnRenderCompleted's errorMessage is untrusted #11843
- All other client-invokable hub methods on
ComponentHub
:-
OnDisconnectedAsync
- OK. If a client chooses to call this unexpectedly, we will just disconnect their circuit. There's already a check to ensure we don't disconnect more than once here.
-
StartCircuit
- Action needed. Currently we don't check whether the hub instance is already associated with a circuit. A client could ask us to start an unlimited number of circuits here within a single connection. Also we don't validate that the supplied
uriAbsolute
/baseUriAbsolute
are non-null valid URLs - I'm not sure how this could be misused, but we should validate since we have no reason not to. In ComponentHub.StartCircuit, prevent creating multiple circuits #11841 Validate uriAbsolute/baseUriAbsolute #11842
- Action needed. Currently we don't check whether the hub instance is already associated with a circuit. A client could ask us to start an unlimited number of circuits here within a single connection. Also we don't validate that the supplied
-
ConnectCircuit
- Action needed. This might be OK already, but it doesn't check whether there's already a circuit associated with this connection. If there is, maybe we need to no-op gracefully instead of calling
_circuitRegistry.ConnectAsync
. Some code inside_circuitRegistry.ConnectAsync
talks about handling this case, but I don't know why it's not just a no-op. In ComponentHub.StartCircuit, prevent creating multiple circuits #11841
- Action needed. This might be OK already, but it doesn't check whether there's already a circuit associated with this connection. If there is, maybe we need to no-op gracefully instead of calling
-
- Anything with
[JSInvokable]
in any of our server-side shipping assemblies-
RendererRegistryEventDispatcher.DispatchEvent
- OK. You can easily cause an exception in parsing, but that's just a normal failed JS interop call. All the state it accesses is per-circuit.
-
RemoteUriHelper.NotifyLocationChanged
- Action needed We don't check the supplied
absoluteUri
string is a valid URL or even non-null. I don't know how you could do anything bad, but we should consider validating that it's a non-null parseable URL. Validate uriAbsolute/baseUriAbsolute #11842
- Action needed We don't check the supplied
-
- Any
static
state in any of our shipping assemblies. This is only tangentally related, but it's worth checking that we don't have any state that clients can read/write in a way that would either disclose info about other circuits or would get the application into state where it would crash or behave badly- See further comments about this below. I've checked throughout .Components, .Server, and .Browser (== .Web). There are some action items there, but I'll check this one off here because no further investigation is needed to find more action items. Blazor: minor tweaks to ensure shared state doesn't cross circuits #11849
- JS interop inbound calls, starting from
- All boundaries between framework and application code within a circuit, to ensure that any exceptions in application code do not kill the entire process. Again, it's fine for an unhandled application exception to kill a circuit, but not the whole process.
- Component construction and initialization / dependency injection
- Creation of
IComponent
instances (e.g., if default constructor throws) - Setters on component instance for DI-injected properties
- Initialization logic in DI services (e.g., if they throw while being instantiated)
- Action needed. All three of the above occur inside
ComponentFactory.InstantiateComponent
, which in turn happens either inside render tree diffing, orCreateInitialRenderAsync
(prerendering), orRemoteRenderer.AddComponentAsync
(for non-prerendered root components). The tree diffing and non-prerendered root component cases at least, we again just pass exceptions to the renderer'sUnhandledException
event, which trusts the client to disconnect, and it can just choose not to. We need the server to enforce disconnection on these errors. Enforce circuit termination on unhandled exception #11845
- Creation of
- Component lifecycle methods from the
IComponent
perspective-
IComponent.Configure
- Action needed. Always called from
Renderer
'sAttachAndInitComponent
, which is called from render tree diffing, non-prerendered root component initialization, and prerendering. Ignoring prerendering (this is investigated elsewhere), exceptions will be handled the same as for the component initialization cases above. Again, this needs action because we're trusting the client to disconnect. Enforce circuit termination on unhandled exception #11845
- Action needed. Always called from
-
IComponent.SetParametersAsync
- Action needed.
SetParametersAsync
is called fromComponentState.SetDirectParameters
andComponentState.NotifyCascadingValueChanged
, both of which capture any exception as a failing task, which is then passed to_renderer.AddToPendingTasks
. The renderer spots failed tasks and calls its ownHandleException
with them, but besides calling that, swallows the exception. As such, rendering attempts to continue and will behave as if nothing's wrong.RemoteRenderer
'sHandleException
does send the exception info to the client, which then disconnects the circuit, which makes sense at development time. But in production, what if a bad client chooses not to disconnect the circuit? Then they continue with the application in an unknown state. This shouldn't be allowed: the server should proactively kill the circuit if there's an unhandled rendering exception. Enforce circuit termination on unhandled exception #11845
- Action needed.
-
- Component lifecycle methods from the
ComponentBase
perspective. Want to be sure the failure modes make sense at this higher level too.-
ComponentBase.ctor
- Duplicate: it's the same asIComponent
's default constructor -
ComponentBase.BuildRenderTree
- Semi-OK: This is called when the framework executesProcessRenderQueue
, which has a big try/catch aroundRenderInExistingBatch
. Exceptions get shuttled to the renderer'sHandleException
, which has problems described elsewhere around letting clients ignore exceptions. Once we fix that, this will be fine, so marking as OK to avoid duplication. -
ComponentBase.OnInit
- Duplicate: this reduces toSetParametersAsync
-
ComponentBase.OnInitAsync
- Duplicate: this reduces toSetParametersAsync
-
ComponentBase.OnParametersSet
- Duplicate: this reduces toSetParametersAsync
-
ComponentBase.OnParametersSetAsync
- Duplicate: this reduces toSetParametersAsync
-
ComponentBase.StateHasChanged
- Duplicate: called either by the developer themselves from other lifecycle methods, or byComponentBase
from its own lifecycle methods. So there's nothing distinct to investigate here. -
ComponentBase.ShouldRender
- Duplicate: called only byComponentBase
itself fromStateHasChanged
-
ComponentBase.OnAfterRender
- Duplicate: this is the implementation ofIHandleAfterRender.OnAfterRenderAsync
-
ComponentBase.OnAfterRenderAsync
- Duplicate: this is the implementation ofIHandleAfterRender.OnAfterRenderAsync
-
ComponentBase.Invoke
- Called by user code. If you're already on the sync context, just invokes the supplied delegate synchronously, so exceptions just go upstream. If you're not on the sync context, you get back aTask
representing the success/exception of your callback, so it becomes up to the caller how exceptions are handled. It's no different from invoking your own action directly. -
ComponentBase.InvokeAsync
- Same behavior asComponentBase.Invoke
, even though some of the internal code paths are different. -
ComponentBase.SetParametersAsync
- Duplicate: this is the implementation ofIComponent.SetParametersAsync
, described above.
-
- Event handler callbacks
-
IHandleEvent.HandleEventAsync
- Action needed. One place this is called from is
BindMethods.DispatchEventAsync
, which comes with a comment saying "this is a temporary polyfill that doesn't do proper error handling and will be removed". Can this be removed now? @rynowak Remove temporary logic from BindMethods.DispatchEventAsync #11846 - Action needed. Besides that, it's called from
EventCallback
/EventCallback<T>
'sInvokeAsync
. The first of these is called byRenderer.DispatchEventAsync
, which is the code path for event notifications from the client (via the JS-invokableRendererRegistryEventDispatcher.DispatchEvent
). It does a try/catch around the callback invocation, and for any exceptions, it calls theRenderer
'sHandleException
which is the same anti-pattern as we see elsewhere whereby it trusts the client to disconnect. Enforce circuit termination on unhandled exception #11845 - Action needed. In the case where the event handler throws synchronously, there's a bug in
Renderer.DispatchEventAsync
. Thetask
variable is left with anull
value, which then throws a nullref exception inGetErrorHandledTask
. Renderer.DispatchEventAsync throws null reference exception if event handler throws synchronously #11847
- Action needed. One place this is called from is
-
- After-render callbacks
-
IHandleAfterRender.OnAfterRenderAsync
- Action needed. This is called from
Renderer.NotifyRenderCompleted
, which has the same anti-pattern as elsewhere, whereby we pass sync/async exceptions to renderer'sHandleException
and trust the client to disconnect. We need the server to enforce circuit termination on exception. Enforce circuit termination on unhandled exception #11845
- Action needed. This is called from
-
- Component and DI service disposal
-
IDisposable.Dispose
called on a component instance- Action needed. In
ComponentState.DisposeInBatch
, there's a comment saying "TODO: Handle components throwing during dispose. Shouldn't break the whole render batch.". I would disagree with this: I would now say that unhandled exceptions in component disposal should abandon rendering and terminate the whole circuit. We should check the behavior is correct and remove the comment. Enforce circuit termination on unhandled exception #11845 - Action needed. Well, non-action needed in this case. When we're disposing a
Renderer
, we loop through all the component instances andDispose
them. If that method fails, we callHandleException
but otherwise swallow the exception. This is the correct behavior, since it's essential we don't fail to dispose other components or finish disposing the renderer itself. However, if we fixHandleException
to force-kill the circuit immediately, we have to be sure we don't break this disposal logic by halting it on the first exception. Enforce circuit termination on unhandled exception #11845
- Action needed. In
- Disposal of DI services injected into a component
- Action needed.
CircuitHost.DisposeAsync
should use try/finally to guarantee that_scope.Dispose
still happens even if any ofOnConnectionDownAsync
/OnCircuitDownAsync
/Renderer.Dispose
throws. Blazor doesn't always dispose the circuit's DI scope #11848
- Action needed.
-
- Routing (probably nothing to dig into here, as routing is implemented as a regular component with no special powers)
- Discovery of components; parsing route templates, etc.
- All OK. The
Router
is just a normal userland component with no special powers, so all its possible failure modes are the same as normal user components. For example, if URL template parsing throws, this reduces to an exception inSetParametersAsync
, so is the same as described above.
- All OK. The
- Discovery of components; parsing route templates, etc.
- Prerendering (make sure some top-level exception handler would catch all sync and async exceptions)
- Action needed. If there's an unhandled exception during
CircuitPrerenderer.PrerenderComponentAsync
, this bubbles up to be the same as an unhandled exception during rendering a Razor page, which is good and correct. However, in this case we fail to callCleanupCircuitState
, so any DI services already injected into the circuit don't get disposed. Blazor doesn't always dispose the circuit's DI scope #11848
- Action needed. If there's an unhandled exception during
- Arbitrary errors at render time, even outside the calls to
BuildRenderTree
etc.- For example, if a component holds on to a
ParameterCollection
instance, it could later mutate it to corrupt the memory inside aRenderTreeBuilder
that was being used by a later render cycle. This could lead to an exception in any part of the rendering system. We must be sure that (1) this can't affect any other circuit (e.g., to crash it, or to read/write state across circuits) and (2) that arbitrary render-time exceptions at worst only bring down one circuit, not the whole process.- Semi-OK. Virtually all of the rendering code runs inside
Renderer.ProcessRenderQueue
, which has a big try/catch and marshals exceptions toHandleException
. This is not completely OK, because of the antipattern described above whereby we're trusting clients then to terminate the connection. Once we fix that so the server enforces immediate circuit termination, this should be OK. Marking as verified to avoid so much duplication. The rendering logic outsideProcessRenderQueue
is pretty much entirely just under the control of the framework, so it's hard to see what you'd do to make it fail, but even if it does throw, it's likely to reduce to being an error on whatever triggered the rendering (usually aStateHasChanged
call fromComponentBase
, which is already described above).
- Semi-OK. Virtually all of the rendering code runs inside
- For example, if a component holds on to a
- Component construction and initialization / dependency injection
To make this tractable, I'll mostly be focused on the low-level responsibility boundaries and ensuring that we are catching and dealing with any exceptions that may occur. Mostly this won't involve tracing into higher-level features. For example it's unnecessary to ask the question "what if an auth policy evaluator throws" because that's something that happens within AuthorizeView
, which is a component that only has the power to do what any other userland component can do.