Harden unsafe controller render defaults#15801
Conversation
Default inspect-style render output to text/plain so unstructured values are not served as HTML. Render file responses as attachments by default, escape unsafe filename characters in Content-Disposition, and document the inline opt-out. Assisted-by: Hephaestus:gpt-5.5
There was a problem hiding this comment.
Pull request overview
This pull request hardens Grails controller render(...) defaults to reduce unsafe browser-rendered responses by defaulting inspect-style renders to text/plain and making file responses download as attachments unless explicitly requested inline.
Changes:
- Default
render(Object)and unrecognizedrender(Map)output totext/plain. - Default file renders to
Content-Disposition: attachment, withinline: trueopt-out and preservation of explicitly setContent-Disposition. - Escape unsafe characters in generated
Content-Dispositionfilenames and document the updated defaults.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| grails-controllers/src/main/groovy/grails/artefact/controller/support/ResponseRenderer.groovy | Implements hardened defaults (text/plain for inspect renders; attachment-by-default for files; filename escaping). |
| grails-controllers/src/test/groovy/grails/artefact/controller/support/ResponseRendererSpec.groovy | Adds focused unit coverage for the new render defaults and header escaping behavior. |
| grails-test-suite-uber/src/test/groovy/org/grails/web/servlet/RenderMethodTests.groovy | Updates/extends integration tests to validate attachment-by-default and inline opt-out for stream renders. |
| grails-doc/src/en/ref/Controllers/render.adoc | Documents the new default behaviors and the inline option for file rendering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## 8.0.x #15801 +/- ##
==================================================
+ Coverage 49.4560% 49.4712% +0.0151%
- Complexity 16640 16684 +44
==================================================
Files 1944 1947 +3
Lines 92379 92468 +89
Branches 16130 16150 +20
==================================================
+ Hits 45687 45745 +58
- Misses 39593 39618 +25
- Partials 7099 7105 +6 🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: 7f1d7a9 Learn more about TestLens at testlens.app. |
| HttpServletResponse response = webRequest.currentResponse | ||
| webRequest.renderView = false | ||
| applyContentType(response, null, object) | ||
| applyContentType(response, null, object, true, 'text/plain') |
There was a problem hiding this comment.
If the content type is unknown, why are we rendering something else?
|
The change ensures that when the content type is unknown, the system defaults to 'text/plain' instead of 'text/html'. This is achieved by updating the grails-controllers/src/main/groovy/grails/artefact/controller/support/ResponseRenderer.groovy |
|
I think to change these defaults we need to adhere to our PR summary policy and discuss the default change more thoroughly after a case has been presented. |
| private boolean applyContentType(HttpServletResponse response, Map argMap, Object renderArgument, boolean useDefault, String defaultContentType) { | ||
| boolean contentTypeIsDefault = true | ||
| String contentType = resolveContentTypeBySourceType(renderArgument, useDefault ? TEXT_HTML : null) | ||
| String contentType = resolveContentTypeBySourceType(renderArgument, useDefault ? defaultContentType : null) |
There was a problem hiding this comment.
I'm not sure we should use a default content type. Especially plain text when encoders could cause non-text values to render.
|
I 100% agree on the filename fixes. The default content type, I'd like to discuss more. |
|
Thanks. I updated the PR body to follow the summary policy more explicitly: it now separates "What was found", "What was fixed", and "Verification". I agree the default behavior change needs maintainer discussion. The intent here is not to render a different semantic object, but to avoid browser HTML interpretation for inspect-style or otherwise unrecognized render output by forcing |
What was found
Several controller
renderpaths used browser-interpreted defaults for output that is not guaranteed to be safe HTML:render(Object)and unrecognizedrender(Map)values could produce inspect-style output without forcing a plain-text response type.render(file: ...)responses could be displayed inline unless callers explicitly supplied a saferContent-Disposition.Content-Dispositionfilenames did not escape unsafe quoted-string characters consistently.That made the default behavior too permissive for applications that render arbitrary objects, maps, or downloaded files without explicitly setting response headers.
What was fixed
text/plain.Content-Disposition: attachment.inline: trueopt-out for intentional inline file rendering.renderreference.Verification
./gradlew.bat --no-daemon :grails-controllers:test --tests "grails.artefact.controller.support.ResponseRendererSpec"./gradlew.bat --no-daemon :grails-test-suite-uber:test --tests "org.grails.web.servlet.RenderMethodTests"./gradlew.bat --no-daemon :grails-doc:publishGuide -x aggregateGroovydoc./gradlew.bat --no-daemon :grails-controllers:test :grails-test-suite-uber:test./gradlew.bat --no-daemon clean aggregateViolations :grails-test-report:check --continue082eaf0baaba9aad69bc8b48f7db19d44bc9ba50.