-
Notifications
You must be signed in to change notification settings - Fork 10.3k
Ensure server-side Blazor failure modes are correct (e.g., clients can't cause a global unhandled exception) #11791
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Yep. Here is a list of stuff
|
OK, @javiercn and I discussed: he's going to deal with resilience of JS interop cases (ensuring we fail in sensible ways and that clients can't cause a global unhandled exception in any case). I'll deal with the prevention of global exceptions in other places. |
@SteveSandersonMS can we also have bullet points for other areas? Even just to confirm that those are handled. I will have to look at other areas no matter what for completeness from the security point of view, I just want the list to be common and in one place, does that make sense? |
Here are a list of all the statics in Still to do: similar investigation for .Server and .Browser Action needed
Readonly never-mutated const-like things
Lazily-initialized but thereafter readonly const-like things
Caches populated using only non-user-specific (and non-APPLICATION-specific) data
Other
|
Also think about whether these cases have good unit tests. Many of these could easily be covered. |
This is a good list 👍 |
Statics in Const-likes
Async-local
|
And for .Browser: Const-likes
Async-local
Action needed
|
OK, I've gone through all the investigations and split out all the action items into separate linked issues. So I'm now unassigning myself from this, leaving the remaining JSInterop bits to be looked at by @javiercn. |
All the JS interop is done as part of #11958 |
OK great, marking this one as done then. |
@SteveSandersonMS When you sign off on my PR :) |
My work here is done #11958 |
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:
ComponentHub.BeginInvokeDotNetFromJS
renderer.DispatchEventAsync
, treating input as untrusted)DispatchEventAsync
becomes a regular failed sync/async JS interop call, so this is scoped to the circuitComponentHub.OnRenderCompleted
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 #11843ComponentHub
:OnDisconnectedAsync
StartCircuit
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 #11842ConnectCircuit
_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[JSInvokable]
in any of our server-side shipping assembliesRendererRegistryEventDispatcher.DispatchEvent
RemoteUriHelper.NotifyLocationChanged
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 #11842static
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 badlyIComponent
instances (e.g., if default constructor throws)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 #11845IComponent
perspectiveIComponent.Configure
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 #11845IComponent.SetParametersAsync
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 #11845ComponentBase
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 constructorComponentBase.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.IHandleEvent.HandleEventAsync
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 #11846EventCallback
/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 #11845Renderer.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 #11847IHandleAfterRender.OnAfterRenderAsync
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 #11845IDisposable.Dispose
called on a component instanceComponentState.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 #11845Renderer
, 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 #11845CircuitHost.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 #11848Router
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.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 #11848BuildRenderTree
etc.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.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).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.The text was updated successfully, but these errors were encountered: