GHSA-9h85-g7w3-rh49 ViewComponent: Reused Component Instances Retain Stale Render Context
Reused Component Instances Retain Stale Render ContextSummary`ViewComponent::Base` instances retain multiple render-scoped objects across calls to `render_in`. If the same component, collection, or spacer component instance is reused across requests, users, tenants, or threads, later renders can use stale `helpers`, `controller`, `request`, `view_flow`, format/variant details, and slot child context from an earlier render.This can cause authorization-aware components to render privileged UI for a lower-privileged user, generate links using a stale Host header, leak slot/helper state, and mix request context under concurrent rendering.SeverityThe PoC demonstrates cross-user authorization impact in a realistic downstream application pattern.
If the receiving program accepts downstream cross-user authorization impact as a scope-changing impact for a framework vulnerability, an alternative High score can be assigned:Alternative CVSS: 8.2
Alternative vector: `CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:N`Affected CodeValidated against:Repository commit: `eea79445`Ruby: `3.4.9`Relevant locations:`lib/view_component/base.rb``render_in``controller``helpers``__vc_request``lib/view_component/slot.rb``Slot#to_s``lib/view_component/slotable.rb`slot storage in `@__vc_set_slots``lib/view_component/collection.rb`child component memoization and spacer renderingKey retained state:Slot children also inherit the parent original view context:Collections memoize child component instances:Root CauseComponent instances are mutable render objects. `render_in` updates some per-render fields, but many request-scoped values are memoized using `||=` or stored for later slot/collection rendering.There is no runtime guard preventing a component instance from being rendered multiple times under different view contexts, and there is no full reset of render-scoped state at the start of each render.Maintainer discussion in prior PRs notes that component instances should not be shared between renders, but the current runtime does not enforce this invariant.Proof of ConceptThe following PoC demonstrates four independent effects:stale authorization gatestale Host/request data in generated absolute URLsstale slot child contextcross-thread context mixingRun from the repository root:Observed output:Authorization-Impact PoCThe following PoC models a realistic downstream application pattern: a shared component registry caches component objects instead of caching component classes, factories, or rendered strings. An admin request primes the cached toolbar component. A later guest request renders the same cached object.The component uses `render?` as an authorization-aware visibility gate and emits a representative privileged action link.Observed output:This confirms a cross-user authorization impact in a realistic pattern: a guest receives privileged UI that a fresh component correctly suppresses. It also confirms stale request and Host context in the generated privileged URL.Exploit ScenarioA downstream app stores component instances in a constant, singleton service, memoized helper, cache object, or shared collection builder to avoid allocation. An attacker or lower-privileged user later triggers rendering of that same object.Potential real-world examples:A navigation/sidebar component checks `helpers.admin?` in `render?`.A tenant switcher uses `request.host` or `current_user.account`.A component emits absolute URLs or signed action links.A table uses slot child components that rely on helper/request state.A global UI registry stores instantiated spacer or child components.In these cases, a component first rendered under an admin or attacker-controlled request can affect later renders for other users.ImpactConfirmed impact classes:stale privileged UI renderingstale user identity through `helpers`stale Host/request data in generated absolute URLsslot child context inheritancecross-thread context corruptionstale format/variant template selectionstale `view_flow` / `content_for` writescollection and spacer component context leakageThis can chain into privilege escalation if an application relies on UI visibility as an authorization boundary. It can also leak signed links, tenant-specific URLs, admin actions, or user-specific data.PreconditionsThe same component, collection, slot, or spacer component instance is reused across render contexts.The component reads request-scoped or user-scoped APIs such as `helpers`, `controller`, `request`, URL helpers, `render?`, `before_render`, slots, variants, formats, or `content_for`.Higher impact when the shared object crosses users, tenants, roles, or threads.Normal per-request usage such as `render(MyComponent.new(...))` is not affected.Chaining PotentialThis issue can chain with:UI-only authorization checkssigned admin links embedded in componentsHost header poisoningmulti-tenant routing based on host/subdomainshared component registriesfragment/component caching patterns that cache objects rather than rendered stringsconcurrent Rails servers such as PumaThe framework alone does not directly prove account takeover, but downstream applications can reach high impact if stale component output exposes privileged action links or bypasses server-side authorization assumptions.RemediationThe safest fix is to make component and collection instances one-shot renderables.Recommended options:Add a runtime guard in `render_in` that raises or warns when the same component instance is rendered again with a different `view_context`.Reset render-scoped ivars at the beginning of every render, including:`__vc_original_view_context``@lookup_context``@view_flow``@__vc_requested_details``@__vc_controller``@__vc_helpers``@__vc_request`Rebuild `ViewComponent::Collection` child component instances per render or document/enforce collections as one-shot.Avoid accepting a reusable instantiated `spacer_component`, or reset/clone it before rendering.Add thread-safety tests for concurrent rendering of a shared instance.
CVSS v3.1 score: 6.8