BalusC commented on PR #2930: URL: https://github.com/apache/shiro/pull/2930#issuecomment-6425403174
Nice. The gate looks right: the view is only rebuilt for a `FormResubmitRequest`, and that request only exists after the same-origin checks. So a forged or cross-site postback cannot trigger a rebuild. A few points. ### M1. On MyFaces, a rebuilt Ajax view is not re-rendered with `@all` `replaySavedForm` still computes `isStateless` with `isJSFStatefulForm`, and that still uses the Mojarra pattern `-?\d+:-?\d+`. The MyFaces server-side token is Base64, so it never matches, and the form counts as stateless. The view handler rebuilds the view anyway, because it does not check `isStateless`. But `AjaxReplay.forForm(isPartial, true)` downgrades `RENDER_ALL` to `PASS_THROUGH`. So on MyFaces an Ajax replay renders only the form's own targets, on a view that was built from scratch. The other forms on the page keep their expired view state. I did not run this on MyFaces; it follows from the code. **Fix:** in `isJSFStatefulForm`, treat any non-blank view state other than the literal `stateless` as stateful. Both Mojarra and MyFaces use `stateless` for a stateless view. Add a unit test with a MyFaces-style Base64 token. ### m2. The view is built while Faces events are off Restore View calls `setProcessingEvents(false)` before `restoreView`, in Mojarra and in MyFaces. So `ComponentsLocal.buildView` runs without `PostAddToViewEvent`. Things like `f:event type="postAddToView"` and the relocation of `h:outputScript target="head"` depend on it. Mojarra turns events on for its own rebuild of a stateless view (`FaceletViewHandlingStrategy`). This matters most when the response renders `@all`. **Fix:** call `context.setProcessingEvents(true)` around `buildView`, and restore the previous value in a `finally`. ### m3. The reason in the `noJSFAjaxRequests` javadoc no longer holds It says the Ajax fields are only kept for stateless views, because there they cannot fail view state restoration. With the view handler, a lost stateful view is rebuilt, so it cannot fail restoration either. The stateless/stateful split still changes behavior: it decides whether the replay executes the whole form or only the partial execute. **Fix:** decide if the split is still wanted. If yes, rewrite the javadoc with the reason that still holds. If no, keep the Ajax fields for both. ### n4. Unrelated change in `FormSupportTest` `parseFormDataHonoursCharset` replaces the `ö` escapes with a literal `ö`. This is not related to #2931, and the test now depends on the source file encoding. **Fix:** revert those two lines. ### n5. `@SneakyThrows(IOException.class)` on `restoreView` This throws a checked exception from a method that does not declare it. Wrapping it in a `FacesException` is the usual way, and then the application's `ExceptionHandler` handles it normally. **Fix:** catch the `IOException` and throw `new FacesException(e)`. Update `buildFailurePropagatesToReplayFallback` to expect that. ### Possible follow-up: drop the in-place replay With this view handler in place, `ShiroFilter` does not need to read the body and dispatch it a second time for an expired same-origin form. It can mark the request in `createSubject` (after the same-origin check), let it continue down the normal filter chain, and let the view handler rebuild the view for marked requests. That would: - keep the request body readable for non-Faces endpoints, such as a servlet that calls `getInputStream()` (today it gets a redirect, because the body was already consumed); - run the remaining filters in the chain once, in the normal order; - remove the unbounded read of the request body into memory. The after-login replay would stay as it is. 🤖 Generated with Claude Opus 5.5 -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
