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.3979% 49.3861% -0.0118%
+ Complexity 16814 16812 -2
==================================================
Files 1993 1993
Lines 93504 93504
Branches 16362 16362
==================================================
- Hits 46189 46178 -11
- Misses 40179 40190 +11
Partials 7136 7136 🚀 New features to boost your workflow:
|
| 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?
There was a problem hiding this comment.
I looked at narrowing the PR to only the filename/disposition fixes and leaving inspect-style render output on the existing default, but review-gate flagged that as reintroducing the exact browser-interpreted inspect-output risk this branch is trying to avoid. The current implementation still renders the same inspect string; it only changes the default response media type for the inspect fallback from browser-interpreted HTML to text/plain unless the caller explicitly sets a content type. I am leaving this thread unresolved for maintainer discussion rather than treating it as resolved.
There was a problem hiding this comment.
Let's discuss in the weekly?
|
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 |
Add a Grails 8 upgrade guide section covering the text/plain inspect default for object and fallback map rendering, the attachment Content-Disposition default with filename escaping for file renders, and the inline and explicit content type opt-outs. Assert the text/plain content type in the controller-level render tests. Assisted-by: opencode:gpt-5.5
|
Pre-release review pass (2026-07-02, dual-reviewer: GPT-5.5 oracle + Codex CLI): NEEDS-IMPROVEMENT, now addressed. Findings: the behavior-breaking render defaults were documented only in the render reference, not the upgrade guide (med), and the controller-level content-type contract was not asserted in |
|
Maintenance update: merged current I also reformatted the PR body around the repository checklist and left the filename/content-type split point explicit: the filename/disposition hardening can be split into a narrower PR if maintainers want to defer discussion of the Verification: |
Resolve the upgrade-guide section-order conflict by keeping both the render default hardening note and the latest 8.0.x upgrade notes. Assisted-by: opencode:gpt-5.5 codex-review
Status updateMerged current Open discussion for weekly meetingMaintainer feedback on the default content-type change for inspect-style
Options for the weekly:
Leaving the branch intact so both protections can be reviewed together until that discussion. |
✅ All tests passed ✅🏷️ Commit: 4b01007 Learn more about TestLens at testlens.app. |
|
Agreed - let's split this so the uncontested part isn't blocked on the debated one. Two independent changes are bundled here:
I'll separate the two: keep the filename/disposition hardening in this PR, and pull the default-content-type change out into its own proposal so we can make the case and discuss it in the weekly without holding up the agreed fixes. For the record (pre-release review): this PR is scoped to unsafe controller render defaults and is explicitly not the broader "GSP encode-by-default" topic (Google Doc 2.4) - that remains separate work. |
Description
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 the caller explicitly supplied a saferContent-Disposition.Content-Dispositionfilenames did not escape unsafe quoted-string characters consistently.What changed
text/plainwhen no explicit content type orresponse.contentTypeis set.Content-Disposition: attachment.inline: truefor intentional inline file rendering.renderreference and Grails 8 upgrade guide.Verification
./gradlew :grails-controllers:test --tests "grails.artefact.controller.support.ResponseRendererSpec" :grails-test-suite-uber:test --tests "org.grails.web.servlet.RenderMethodTests" :grails-doc:publishGuide -x aggregateGroovydoc8.0.x; the only conflict was the upgrade-guide section ordering, resolved by preserving both the render-hardening note and the new upstream@EnableWebMvc/ bean-overriding notes.Open discussion
Maintainer feedback agrees with the filename/disposition hardening and asks for more discussion on the default content-type change. I left the branch intact so both protections can be reviewed together, but the filename/disposition part can be split into a narrower PR if maintainers prefer to defer the content-type default.
Contributor Checklist
Issue and Scope
8.0.x, where breaking default changes are permitted for discussion.Code Quality
Licensing and Attribution
Documentation