Lightning setup: form-bound StoreId on POST re-render can point view extensions at another store
Found during a hardening review of the Flint plugin (sethforprivacy/flint). Reporting here since the general weakness is a core view-model binding gap; adjacent hardening landed in #7513.
Summary
UIStoresController.SetupLightningNode (LightningLike) binds LightningNodeViewModel from the POST form and never assigns vm.StoreId from the authorised route value (the GET handler does). On the validation-failure re-renders (return View(vm) — currently on six paths: missing connection string, invalid model state after handler validation, invalid network, test-connection failure, test-connection success, unknown command default), the page's UI-extension points (ln-payment-method-setup-tabhead, ln-payment-method-setup-tab, ln-payment-method-setup-custom) receive @Model with the form-supplied StoreId.
A plugin extension point that resolves resources by that id therefore acts on a store the current request was not authorised for. LightningNodeViewModel.StoreId (BTCPayServer/Models/StoreViewModels/LightningNodeViewModel.cs) is a plain string property without [BindNever].
Reproduction (Flint-specific instance of the general shape)
With the Flint plugin: user authenticated with canmodifystoresettings on their own store A only; another store B on the same server has the plugin configured. POST /stores/{A}/lightning/BTC/setup with StoreId={B} and an empty ConnectionConfiguration — core authorises against the route's store A, fails "Please provide a connection string", and re-renders with the form-bound StoreId={B}; the tabhead extension point resolved against B and rendered B's connection string (a bearer spend credential in Flint's data model) into the error page's payload.
The same shape applies to any extension point (or component) that trusts Model.StoreId on that page without re-checking the request's authorised store. Core puts the authoritative store on the HttpContext (BuiltInPermissionHandler stashes it at authorisation on both GET and POST, and the global SetContextFilter promotes it), but nothing ties the re-rendered model to it.
Suggested fixes (non-exclusive)
[BindNever]onLightningNodeViewModel.StoreId(or overwritingvm.StoreIdfrom the route value in the POST handler before any re-render), so the form can never substitute the store the page renders extensions for.- Optionally: core documentation of the trap for plugin authors — "extension-point models carry form data, not the authorised store" — plugin-side mitigations still have to exist regardless.
What exists today
- BTCPay's
SetContextFilter+BuiltInPermissionHandleralready place the authorised store on theHttpContext(GetStoreDataOrNull()), so plugin-side mitigations are possible — Flint ships one (PR sethforprivacy/flint#56, verified end-to-end against a live v2.4.2 release). - #7513 (post-2.4.3) passes the authorised store into payment-method config validation, which hardens the save seam; the render seam above stays form-bound.
Happy to turn this into a core PR for the [BindNever]/route-assignment change if maintainers prefer that route.
Source: btcpayserver/btcpayserver