matrei commented on PR #16149:
URL: https://github.com/apache/grails-core/pull/16149#issuecomment-5471155801

   # AI Review
   
   Compared the checked-out branch at `a192c75725` with `origin/8.0.x` at 
`4309d5b876`.
   
   ## Findings
   
   ### 1. High: Dispatcher-level hidden-method overrides are not propagated to 
`GrailsWebRequest`
   
   References:
   
   - 
`grails-web-mvc/src/main/groovy/org/grails/web/servlet/mvc/GrailsDispatcherServlet.groovy:115-122`
   - 
`grails-web-common/src/main/groovy/org/grails/web/servlet/mvc/GrailsWebRequest.java:269-270`
   - 
`grails-web-common/src/main/groovy/grails/web/api/ServletAttributes.groovy:47-49`
   
   `checkMultipart()` returns a wrapper reporting `PUT`, `PATCH`, or `DELETE`, 
but the `GrailsWebRequest` was already bound to the original request and 
continues returning it. The override is only exposed through an attribute 
consumed by `HiddenHttpMethod.effectiveMethod()`.
   
   As a result:
   
   - URL mapping resolution can select `PUT`, `PATCH`, or `DELETE`.
   - `allowedMethods` sees the overridden method.
   - `controller.request.method` still reports `POST`.
   - `ServletRenderContext.getHttpMethod()` still reports `POST`.
   - Command-object initialization still branches on `POST`.
   - Method-keyed mapping names still use `POST`.
   
   This contradicts the implementation comments and documentation claiming that 
the Grails request API agrees with the selected route. The existing test 
already expects `request.method == 'PUT'` in 
`grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy:334-337`,
 but the current lifecycle supplies the original request to the controller.
   
   This should block approval until the request method authority is unified, or 
the behavior is explicitly redesigned and all affected APIs are updated.
   
   ### 2. High: Multipart form fields can be lost from `params`
   
   Reference:
   
   - 
`grails-web-common/src/main/groovy/grails/web/servlet/mvc/GrailsParameterMap.java:99-117`
   
   The new code reads ordinary parameters from the outer request, then 
discovers the inner `MultipartHttpServletRequest` but only merges its uploaded 
files. It does not merge the multipart wrapper's parameter map.
   
   Spring's `StandardMultipartHttpServletRequest` explicitly documents that 
servlet containers are not guaranteed to expose multipart text parts through 
the native request's `getParameterMap()`. Its own `getParameterMap()` merges 
those parts. Therefore, with a container exhibiting that behavior, fields such 
as `description`, `category`, or `_method` disappear from Grails `params`.
   
   The existing `uploadWithMetadata` integration test expects multipart text 
fields to be present, but coverage uses only the normal container path and does 
not verify the outer-wrapper/native-parameter-map combination.
   
   `GrailsParameterMap` should merge parameters from the resolved multipart 
request as well as files, while retaining the tolerant behavior for an 
unparseable oversized request.
   
   ### 3. High: The URL-mapping fallback resolves hidden methods during 
forwards and includes
   
   References:
   
   - 
`grails-web-mvc/src/main/groovy/org/grails/web/servlet/mvc/GrailsDispatcherServlet.groovy:93,115`
   - 
`grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/mvc/UrlMappingsHandlerMapping.groovy:89-94,178`
   
   The dispatcher deliberately disables hidden-method resolution for 
forwards/includes. However, `UrlMappingsHandlerMapping.resolveHttpMethod()` 
still reads `_method` for every mapping lookup whenever 
`resolveHiddenHttpMethod` is enabled.
   
   A forwarded or included `POST` carrying `_method=DELETE` can therefore be 
matched as `DELETE`, even though the dispatcher did not apply the override. 
This can produce inconsistent routing and `allowedMethods` behavior, and may 
let inherited request parameters influence an internal dispatch's action 
selection.
   
   The handler mapping should apply the same dispatch-type guard as the 
dispatcher, or the dispatcher and mapping behavior should be made intentionally 
consistent.
   
   ### 4. High: Generic PUT/PATCH forms no longer preserve their requested 
method
   
   Reference:
   
   - 
`grails-gsp/plugin/src/main/groovy/org/grails/plugins/web/taglib/FormTagLib.groovy:519-525`
   
   With the filter disabled, `_method` is suppressed for every PUT/PATCH form, 
not just forms targeting a resource member URL. This changes:
   
   ```groovy
   <g:form url="/admin/update" method="PUT">
   ```
   
   from a POST containing `_method=PUT` into a plain POST. Existing custom 
PUT/PATCH-only mappings consequently stop matching.
   
   The newly generated POST member route only compensates for `resources` 
mappings. It cannot preserve arbitrary application mappings. The form tag 
either needs to retain `_method` for non-resource forms or determine that the 
target is covered by the new resource fallback.
   
   ### 5. High: Bare member POST introduces a new mutation route with 
potentially different authorization semantics
   
   References:
   
   - 
`grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/DefaultUrlMappingEvaluator.java:147-155,960-967`
   - 
`grails-rest-transforms/src/main/groovy/grails/rest/RestfulController.groovy:44`
   
   The branch adds:
   
   ```text
   POST /books/$id -> update
   ```
   
   for every `resources` mapping when filter mode is disabled. A bare POST to a 
member URL can now invoke `update`, whereas it did not previously.
   
   The PR rationale says this does not widen authorization because security 
sees both requests as POST. However, applications commonly distinguish 
collection POST and member POST, and may have authorization rules that permit 
collection POST while unintentionally allowing member POST. This is a new write 
route and should be treated as a security-sensitive behavior change.
   
   The upgrade documentation currently understates this consequence.
   
   ### 6. Medium: `SpringSecurityUtils.isAjax()` still performs an unsafe 
multipart parameter read
   
   Reference:
   
   - 
`grails-spring-security/plugin/src/main/groovy/grails/plugin/springsecurity/SpringSecurityUtils.groovy:303-310`
   
   The method directly calls:
   
   ```groovy
   request.getParameter('ajax')
   ```
   
   before the tolerant multipart handling. For an oversized multipart request, 
this can fail inside the Spring Security filter chain, before 
`DispatcherServlet.checkMultipart()` can route the multipart exception through 
Grails error handling.
   
   The direct parameter read should use `WebUtils.readParameter()`, and Spring 
Security integration coverage should include oversized multipart requests 
handled by authentication and access-denied flows.
   
   ### 7. Medium: `HttpMethodOverrideDetector` remains inconsistent with the 
tolerant parameter path
   
   Reference:
   
   - 
`grails-spring-security/plugin/src/main/groovy/grails/plugin/springsecurity/web/filter/HttpMethodOverrideDetector.groovy:46-52`
   
   This Spring Security extension point still calls `request.getParameter()` 
directly. If configured ahead of dispatcher multipart handling, an oversized 
multipart request can fail in the filter chain rather than reaching the 
intended application error pipeline.
   
   No default production call site was found, so this is primarily an extension 
compatibility risk, but it should either use the tolerant helper or be 
documented as intentionally outside the new behavior.
   
   ### 8. Medium: Interceptor post-processing now mutates request state in place
   
   Reference:
   
   - 
`grails-interceptors/src/main/groovy/org/grails/plugins/web/interceptors/GrailsInterceptorHandlerInterceptorAdapter.groovy:118-120`
   
   The previous implementation created a reversed copy of the 
matched-interceptor list. The new implementation reverses the request attribute 
in place.
   
   A second `postHandle()` call now reverses the list back, changing callback 
order between invocations. The existing spec already invokes `postHandle()` 
twice, but only checks the model/view result, not the callback order. Nested 
dispatches, forwards/includes, async redispatches, or custom lifecycle 
invocations can expose this difference.
   
   The list should be copied before reversal, or the lifecycle should 
explicitly guarantee that `postHandle()` is called only once.
   
   ## Additional review notes
   
   - The branch is clean and is 53 commits ahead of `origin/8.0.x` at the 
refreshed head `a192c75725`.
   - The GORM/native-ID changes visible in the branch are already present in 
`origin/8.0.x`; they are not additional PR changes.
   - The captured CI state was not fully green. CodeQL reported 11 new 
high-severity alerts, and the captured merge state was unstable.
   - Patch coverage was approximately 69.7%, with 134 changed lines not covered.
   - The most important missing coverage is a full request-path test combining 
filter ordering, multipart resolution, dispatcher method wrapping, URL mapping, 
`GrailsWebRequest`, controller request access, `allowedMethods`, command-object 
binding, REST rendering, and Spring Security.
   


-- 
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]

Reply via email to