From bfdbbe88fa63c33b92e3a0ed7e0509ef99b500ac Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Mon, 20 Jul 2026 13:08:24 +0200 Subject: [PATCH 01/22] PR for: https://github.com/apache/grails-core/issues/15644 --- .../ControllersAutoConfiguration.java | 43 ++++++++++--------- .../ControllersAutoConfigurationSpec.groovy | 24 +++++++++++ .../main/groovy/grails/config/Settings.groovy | 20 --------- .../controllers/uploadingFiles.adoc | 19 ++++---- .../spring-configuration-metadata.json | 24 ----------- 5 files changed, 58 insertions(+), 72 deletions(-) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 1d7527b2d78..e51afd8990f 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -26,18 +26,26 @@ import jakarta.servlet.Filter; import jakarta.servlet.MultipartConfigElement; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; import org.springframework.boot.servlet.autoconfigure.HttpEncodingAutoConfiguration; import org.springframework.boot.servlet.filter.OrderedCharacterEncodingFilter; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean; +import org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletRegistrationBean; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import org.springframework.context.ApplicationContext; +import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Bean; +import org.springframework.core.env.Environment; import org.springframework.util.ClassUtils; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.DispatcherServlet; @@ -58,7 +66,15 @@ after = {GrailsDomainClassAutoConfiguration.class} ) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -public class ControllersAutoConfiguration { +public class ControllersAutoConfiguration implements EnvironmentAware { + + private static final String LEGACY_MULTIPART_CONFIGURATION = "grails.controllers.upload"; + + static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = + "Configuration properties under 'grails.controllers.upload' are no longer supported. " + + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + + "'spring.servlet.multipart.max-file-size=200MB' and " + + "'spring.servlet.multipart.max-request-size=200MB'."; @Value("${" + Settings.FILTER_ENCODING + ":utf-8}") private String filtersEncoding; @@ -75,18 +91,6 @@ public class ControllersAutoConfiguration { @Value("${" + Settings.RESOURCES_PATTERN + ":" + Settings.DEFAULT_RESOURCE_PATTERN + "}") private String resourcesPattern; - @Value("${" + Settings.CONTROLLERS_UPLOAD_LOCATION + ":#{null}}") - private String uploadTmpDir; - - @Value("${" + Settings.CONTROLLERS_UPLOAD_MAX_FILE_SIZE + ":128000}") - private long maxFileSize; - - @Value("${" + Settings.CONTROLLERS_UPLOAD_MAX_REQUEST_SIZE + ":128000}") - private long maxRequestSize; - - @Value("${" + Settings.CONTROLLERS_UPLOAD_FILE_SIZE_THRESHOLD + ":0}") - private int fileSizeThreshold; - @Value("${" + Settings.WEB_SERVLET_PATH + ":#{null}}") String grailsServletPath; @@ -150,12 +154,11 @@ public FilterRegistrationBean grailsWebRequestFilter(Gra return registrationBean; } - @Bean - public MultipartConfigElement multipartConfigElement() { - if (uploadTmpDir == null) { - uploadTmpDir = System.getProperty("java.io.tmpdir"); + @Override + public void setEnvironment(Environment environment) { + if (Binder.get(environment).bind(LEGACY_MULTIPART_CONFIGURATION, Bindable.mapOf(String.class, Object.class)).isBound()) { + throw new IllegalStateException(LEGACY_MULTIPART_CONFIGURATION_ERROR); } - return new MultipartConfigElement(uploadTmpDir, maxFileSize, maxRequestSize, fileSizeThreshold); } @Bean @@ -164,7 +167,7 @@ public DispatcherServlet dispatcherServlet() { } @Bean - public DispatcherServletRegistrationBean dispatcherServletRegistration(GrailsApplication application, DispatcherServlet dispatcherServlet, MultipartConfigElement multipartConfigElement) { + public DispatcherServletRegistrationBean dispatcherServletRegistration(GrailsApplication application, DispatcherServlet dispatcherServlet, ObjectProvider multipartConfigElement) { if (grailsServletPath == null) { boolean isTomcat = ClassUtils.isPresent("org.apache.catalina.startup.Tomcat", application.getClassLoader()); grailsServletPath = isTomcat ? Settings.DEFAULT_TOMCAT_SERVLET_PATH : Settings.DEFAULT_WEB_SERVLET_PATH; @@ -172,7 +175,7 @@ public DispatcherServletRegistrationBean dispatcherServletRegistration(GrailsApp DispatcherServletRegistrationBean dispatcherServletRegistration = new DispatcherServletRegistrationBean(dispatcherServlet, grailsServletPath); dispatcherServletRegistration.setLoadOnStartup(2); dispatcherServletRegistration.setAsyncSupported(true); - dispatcherServletRegistration.setMultipartConfig(multipartConfigElement); + multipartConfigElement.ifAvailable(dispatcherServletRegistration::setMultipartConfig); return dispatcherServletRegistration; } diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index 1d2c17b61f1..b5ef227a8d8 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -19,6 +19,8 @@ package org.grails.plugins.web.controllers +import org.springframework.beans.factory.BeanCreationException +import org.springframework.core.env.MapPropertySource import java.util.function.Supplier import grails.core.DefaultGrailsApplication @@ -35,6 +37,7 @@ import org.springframework.context.ConfigurableApplicationContext import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse import org.springframework.mock.web.MockServletContext +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext import org.springframework.web.context.WebApplicationContext import org.springframework.web.context.support.StaticWebApplicationContext import org.springframework.web.filter.RequestContextFilter @@ -54,6 +57,27 @@ class ControllersAutoConfigurationSpec extends Specification { def autoConfiguration = new ControllersAutoConfiguration() + def "legacy multipart configuration fails startup with migration instructions"() { + given: + def applicationContext = new AnnotationConfigWebApplicationContext() + applicationContext.servletContext = new MockServletContext() + applicationContext.environment.propertySources.addFirst(new MapPropertySource('test', [ + 'grails.controllers.upload.maxFileSize': 20000000, + ])) + applicationContext.register(ControllersAutoConfiguration) + + when: + applicationContext.refresh() + + then: + BeanCreationException exception = thrown() + exception.rootCause instanceof IllegalStateException + exception.rootCause.message == ControllersAutoConfiguration.LEGACY_MULTIPART_CONFIGURATION_ERROR + + cleanup: + applicationContext.close() + } + void 'grailsWebRequest filter is a RequestContextFilter so Boot WebMvcAutoConfiguration backs off its own RequestContextFilter'() { when: 'the Grails request-binding filter bean is created' GrailsWebRequestFilter filter = autoConfiguration.grailsWebRequest(applicationContext) diff --git a/grails-core/src/main/groovy/grails/config/Settings.groovy b/grails-core/src/main/groovy/grails/config/Settings.groovy index 6acf468f8b8..ff65c2a40bc 100644 --- a/grails-core/src/main/groovy/grails/config/Settings.groovy +++ b/grails-core/src/main/groovy/grails/config/Settings.groovy @@ -209,26 +209,6 @@ interface Settings { */ String CONTROLLERS_DEFAULT_SCOPE = 'grails.controllers.defaultScope' - /** - * The upload directory for controllers, defaults to java.tmp.dir - */ - String CONTROLLERS_UPLOAD_LOCATION = 'grails.controllers.upload.location' - - /** - * The maximum file size - */ - String CONTROLLERS_UPLOAD_MAX_FILE_SIZE = 'grails.controllers.upload.maxFileSize' - - /** - * The maximum request size - */ - String CONTROLLERS_UPLOAD_MAX_REQUEST_SIZE = 'grails.controllers.upload.maxRequestSize' - - /** - * The file size threshold - */ - String CONTROLLERS_UPLOAD_FILE_SIZE_THRESHOLD = 'grails.controllers.upload.fileSizeThreshold' - /** * The encoding to use for filters, default to UTF-8 */ diff --git a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc index 606232098e7..2d0bf980690 100644 --- a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc +++ b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc @@ -91,7 +91,7 @@ class Image { ==== Increase Upload Max File Size -Grails default size for file uploads is 128000 (~128KB). When this limit is exceeded you'll see the following exception: +Spring Boot's default size for file uploads is 1MB and its default maximum multipart request size is 10MB. When a limit is exceeded you'll see the following exception: [source,java] ---- @@ -103,16 +103,19 @@ You can configure the limit in your `application.yml` as follows: [source,yml] .grails-app/conf/application.yml ---- -grails: - controllers: - upload: - maxFileSize: 2000000 - maxRequestSize: 2000000 +spring: + servlet: + multipart: + max-file-size: 200MB + max-request-size: 200MB ---- -`maxFileSize` = The maximum size allowed for uploaded files. +`max-file-size` = The maximum size allowed for an uploaded file. -`maxRequestSize` = The maximum size allowed for multipart/form-data requests. +`max-request-size` = The maximum size allowed for a multipart/form-data request. + +The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `file-size-threshold`, and `enabled` properties. +The former `grails.controllers.upload` configuration is no longer supported. Applications using it fail at startup with a message explaining how to migrate to `spring.servlet.multipart`. You should keep in mind https://www.owasp.org/index.php/Unrestricted_File_Upload[OWASP recommendations - Unrestricted File Upload] diff --git a/grails-web-core/src/main/resources/META-INF/spring-configuration-metadata.json b/grails-web-core/src/main/resources/META-INF/spring-configuration-metadata.json index 831f5866b67..ced384e6567 100644 --- a/grails-web-core/src/main/resources/META-INF/spring-configuration-metadata.json +++ b/grails-web-core/src/main/resources/META-INF/spring-configuration-metadata.json @@ -52,30 +52,6 @@ "description": "The default scope for controllers (singleton, prototype, session).", "defaultValue": "singleton" }, - { - "name": "grails.controllers.upload.location", - "type": "java.lang.String", - "description": "The directory for temporary file uploads.", - "defaultValue": "System.getProperty('java.io.tmpdir')" - }, - { - "name": "grails.controllers.upload.maxFileSize", - "type": "java.lang.Integer", - "description": "Maximum file size for uploads (in bytes).", - "defaultValue": 1048576 - }, - { - "name": "grails.controllers.upload.maxRequestSize", - "type": "java.lang.Integer", - "description": "Maximum request size for multipart uploads (in bytes).", - "defaultValue": 10485760 - }, - { - "name": "grails.controllers.upload.fileSizeThreshold", - "type": "java.lang.Integer", - "description": "File size threshold (in bytes) above which uploads are written to disk.", - "defaultValue": 0 - }, { "name": "grails.web.url.converter", "type": "java.lang.String", From d1ca11fc663192167367efae9e14874684d03a70 Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Mon, 20 Jul 2026 17:29:43 +0200 Subject: [PATCH 02/22] Merge fixes & cleanups --- THREAT_MODEL.md | 6 +++--- .../controllers/ControllersAutoConfiguration.java | 7 ++----- .../ControllersAutoConfigurationSpec.groovy | 6 +++--- threat-model.yaml | 14 +++++++------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 1452a1b84e2..bb098481cf7 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -191,7 +191,7 @@ The framework exposes a small number of configuration knobs whose value affects | `grails.databinding.autoGrowCollectionLimit` | 256 *(documented: [`SimpleDataBinder.groovy`](./grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy))* | Caps automatic collection growth during data binding - hard limit on memory amplification from an attacker submitting deeply indexed parameters (`list[1000000]=x`). Raising removes the cap. | **§14 wave 2** - is the documented default the supported production posture, or is the operator expected to lower it? | | `grails.databinding.dateFormats` / `dateParsingLenient` | RFC-3339 + locale defaults; lenient parsing on | Affects how strict date binding is. Loose parsing has historically been a source of validation-bypass findings in other frameworks. | **§14 wave 2** | | `grails.views.default.codec` and codec defaults (`grails.views.gsp.codecs.expression`, `scriptlet`, `taglib`, `staticparts`) | `html` for expression / scriptlet contexts (XSS protection on by default) | Setting any of these to `none` **disables automatic output encoding** for that context - immediate `OUT-OF-MODEL: non-default-build` for XSS reports under non-default settings. *(documented: [xssPrevention.adoc](./grails-doc/src/en/guide/security/xssPrevention.adoc), [codecs.adoc](./grails-doc/src/en/guide/security/codecs.adoc))* | **§14 wave 1** - confirm the `html` default is the supported production posture. | -| `grails.controllers.upload.maxFileSize` / `maxRequestSize` | **128000 bytes (~125 KB) each**, set by `ControllersAutoConfiguration` (overrides Spring Boot's `MultipartProperties` defaults). *(documented: [`ControllersAutoConfiguration.java`](./grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java))* | Multipart upload size cap. Operators who raise these past their application's actual need expose themselves to DoS via large multipart bodies. | **§14 wave 2** | +| `spring.servlet.multipart.max-file-size` / `max-request-size` | **1 MB per file / 10 MB per request**, provided by Spring Boot's `MultipartProperties` defaults. *(documented: [uploadingFiles.adoc](./grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc))* | Multipart upload size cap. Operators who raise these past their application's actual need expose themselves to DoS via large multipart bodies. | **§14 wave 2** | | `grails.allowedMethods` (per-controller) | None (developer opt-in) | Restricts HTTP methods accepted by each action. Absence is **not** a finding; the model treats per-action method gating as a developer responsibility. *(inferred)* | **§14 wave 1** | | `grails.config.locations` (env var, system property, or config) | Empty | Adds external config file paths. **A non-empty value sourced from an untrusted location is a `BY-DESIGN: property-disclaimed` triage outcome** - see §9. | **§14 wave 1** - confirm this disposition. | | `GRAILS_ENV` / `grails.env` | `development` from CLI, `production` for assembled bootJars | Selects the active environment block in `application.yml` / `application.groovy`. Operators who deploy with `GRAILS_ENV=development` inherit the looser dev defaults (e.g., stack traces in responses). | **§14 wave 1** - is deploying with `development` a `non-default-build` posture? | @@ -212,7 +212,7 @@ The framework's public input boundary is the HTTP request. Per-parameter trust i | `Controller.params` | All values | **Yes** - direct request parameter map | Type coercion correctness; never concatenate into HQL/SQL/JPQL/Groovy strings; never use as redirect target without an allow-list. *(documented: [securingAgainstAttacks.adoc](./grails-doc/src/en/guide/security/securingAgainstAttacks.adoc) "XSS", "HTML/URL injection")* | | `Controller.request.headers` | All values | **Yes** - including `X-Forwarded-*`, `Host`, `User-Agent`, `Referer`, custom auth tokens | Treat presence as evidence of nothing; auth headers must be verified against the configured auth subsystem (Spring Security or equivalent). *(inferred)* | | `Controller.request.cookies` | All values | **Yes** | Treat as attacker-supplied; if used for auth, integrity-protect via Spring Security or signed cookies. *(inferred)* | -| `Controller.request.JSON` / `XML` | Full body | **Yes** | Parser inputs are bounded by `maxRequestSize`; nested-depth limits are the parser's responsibility (Jackson, JAXP). *(inferred)* | +| `Controller.request.JSON` / `XML` | Full body | **Yes** | Configure server request-size limits appropriate for the deployment; nested-depth limits are the parser's responsibility (Jackson, JAXP). *(inferred)* | | `bindData(target, source)` | `source` (any `Map` or request) | **Yes** for the source; **No** for the target type (developer-controlled) | Use `bindable`/`include`/`exclude` to whitelist fields. The framework will bind every settable property of `target` from matching keys in `source` unless told otherwise. *(documented: [GORM data binding guide](https://grails.apache.org/docs/latest/guide/single.html#dataBinding))* | | Command-object binding (auto-bound controller action parameter) | Field values | **Yes** | Annotate command-object fields with `bindable=false` for fields that must not be set from the request. *(inferred)* | | Domain-class binding (`new Book(params)`, `book.properties = params`) | Field values | **Yes** | **Mass-assignment risk.** Use command objects or explicit allow-lists rather than binding the request to a domain class. *(inferred)* | @@ -295,7 +295,7 @@ Each property is stated with its conditions, the symptom of a violation, a sever | P6 | **Data binding respects `bindable=false` and explicit `include`/`exclude` lists.** | [CWE-915](https://cwe.mitre.org/data/definitions/915.html) | Field is annotated or the binding call explicitly lists allowed/forbidden fields. | A field marked unbindable is set from request input. | **Security-critical (CVE-eligible)** | *(inferred)* | | P7 | **Compile-time AST transforms (`@Resource`, `@Validateable`, etc.) only act on developer-authored source.** | [CWE-94](https://cwe.mitre.org/data/definitions/94.html) | Build runs on developer-controlled source. | A transform fires on or is influenced by attacker-supplied input. | **Correctness** (security-critical only if reachable from a non-build attacker) | *(inferred)* | | P8 | **Configuration loading does not evaluate `application.groovy` from a path the framework itself chose at runtime - paths come from build-time classpath and operator-supplied environment/system properties.** | [CWE-94](https://cwe.mitre.org/data/definitions/94.html) | Operator has not pointed `grails.config.locations` at attacker-writable storage. | A user request causes evaluation of a Groovy file the operator did not authorize. | **Security-critical (CVE-eligible)** if violated. | *(inferred)* (§14 wave 1) | -| P9 | **`maxFileSize` / `maxRequestSize` / `autoGrowCollectionLimit` provide bounded data-binding memory.** | [CWE-770](https://cwe.mitre.org/data/definitions/770.html) | Operator does not raise the limits past application needs. | Memory growth proportional to attacker-controlled input regardless of limit. | **Resource bug** | *(inferred)* (§14 wave 2) | +| P9 | **Multipart upload limits and `autoGrowCollectionLimit` bound request-processing memory.** | [CWE-770](https://cwe.mitre.org/data/definitions/770.html) | Operator does not raise the limits past application needs. | Memory growth proportional to attacker-controlled input regardless of limit. | **Resource bug** | *(inferred)* (§14 wave 2) | ### Resource consumption line diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index e51afd8990f..8bb61519748 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -31,13 +31,10 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.servlet.autoconfigure.HttpEncodingAutoConfiguration; -import org.springframework.boot.servlet.filter.OrderedCharacterEncodingFilter; -import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletRegistrationBean; -import org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration; -import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.boot.servlet.autoconfigure.HttpEncodingAutoConfiguration; +import org.springframework.boot.servlet.filter.OrderedCharacterEncodingFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.DispatcherServletRegistrationBean; diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index b5ef227a8d8..fb90f00b612 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -19,13 +19,12 @@ package org.grails.plugins.web.controllers -import org.springframework.beans.factory.BeanCreationException -import org.springframework.core.env.MapPropertySource import java.util.function.Supplier import grails.core.DefaultGrailsApplication import grails.core.GrailsApplication +import org.springframework.beans.factory.BeanCreationException import org.springframework.boot.autoconfigure.AutoConfigurations import org.springframework.boot.test.context.runner.WebApplicationContextRunner import org.springframework.boot.web.servlet.AbstractFilterRegistrationBean @@ -34,11 +33,12 @@ import org.springframework.boot.web.servlet.ServletContextInitializerBeans import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration import org.springframework.context.ApplicationContext import org.springframework.context.ConfigurableApplicationContext +import org.springframework.core.env.MapPropertySource import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse import org.springframework.mock.web.MockServletContext -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext import org.springframework.web.context.WebApplicationContext +import org.springframework.web.context.support.AnnotationConfigWebApplicationContext import org.springframework.web.context.support.StaticWebApplicationContext import org.springframework.web.filter.RequestContextFilter import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver diff --git a/threat-model.yaml b/threat-model.yaml index 0a2a84edb8d..8a3faa414f3 100644 --- a/threat-model.yaml +++ b/threat-model.yaml @@ -124,17 +124,17 @@ config_knobs: security_relevant: true maintainer_stance: unresolved section: "§5a" - - name: grails.controllers.upload.maxFileSize - default: 128000 + - name: spring.servlet.multipart.max-file-size + default: 1048576 default_units: bytes - default_source: grails-controllers/.../ControllersAutoConfiguration.java + default_source: Spring Boot MultipartProperties security_relevant: true maintainer_stance: unresolved section: "§5a" - - name: grails.controllers.upload.maxRequestSize - default: 128000 + - name: spring.servlet.multipart.max-request-size + default: 10485760 default_units: bytes - default_source: grails-controllers/.../ControllersAutoConfiguration.java + default_source: Spring Boot MultipartProperties security_relevant: true maintainer_stance: unresolved section: "§5a" @@ -298,7 +298,7 @@ properties_provided: provenance: inferred open_question: "§14 wave 1" - id: P9 - description: "maxFileSize, maxRequestSize, autoGrowCollectionLimit provide bounded data-binding memory." + description: "Multipart upload limits and autoGrowCollectionLimit bound request-processing memory." cwe: CWE-770 conditions: "Operator does not raise the limits past application needs." violation_symptom: "Memory growth proportional to input regardless of configured limit." From 3a07ad21f3dce03ba9c8dcfefa5ff273d3c141fd Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Wed, 22 Jul 2026 19:34:45 +0200 Subject: [PATCH 03/22] Implemented @matrei feedback --- .../web/controllers/ControllersAutoConfiguration.java | 2 +- .../controllers/ControllersAutoConfigurationSpec.groovy | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 8bb61519748..98f1fc7feeb 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -67,7 +67,7 @@ public class ControllersAutoConfiguration implements EnvironmentAware { private static final String LEGACY_MULTIPART_CONFIGURATION = "grails.controllers.upload"; - static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = + private static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + "'spring.servlet.multipart.max-file-size=200MB' and " + diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index fb90f00b612..09f22fc8c8c 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -70,9 +70,13 @@ class ControllersAutoConfigurationSpec extends Specification { applicationContext.refresh() then: - BeanCreationException exception = thrown() + def exception = thrown(BeanCreationException) exception.rootCause instanceof IllegalStateException - exception.rootCause.message == ControllersAutoConfiguration.LEGACY_MULTIPART_CONFIGURATION_ERROR + exception.rootCause.message == + "Configuration properties under 'grails.controllers.upload' are no longer supported. " + + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + + "'spring.servlet.multipart.max-file-size=200MB' and " + + "'spring.servlet.multipart.max-request-size=200MB'." cleanup: applicationContext.close() From b255caadbc5527d0c694660fe3f1dbf6dc5e564b Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Thu, 23 Jul 2026 11:08:15 +0200 Subject: [PATCH 04/22] Upgrade guide --- .../src/en/guide/upgrading/upgrading80x.adoc | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 2bb87483681..4fefb929780 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -109,7 +109,9 @@ org.springframework.boot.env.EnvironmentPostProcessor=com.example.MyPostProcesso org.springframework.boot.EnvironmentPostProcessor=com.example.MyPostProcessor ---- -==== 4. MongoDB Configuration Property Changes +==== 4. Spring Boot Configuration Property Changes + +===== 4.1 MongoDB Configuration Spring Boot 4 renamed the MongoDB configuration property namespace for Spring Boot's own auto-configuration. If your `application.yml` or `application.groovy` uses `spring.data.mongodb.*` properties for Spring Boot auto-configuration, you must rename them to `spring.mongodb.*`. @@ -136,6 +138,34 @@ spring: database: mydb ---- +===== 4.2 Multipart Upload Configuration + +The `grails.controllers.upload.*` configuration properties are no longer supported. +Applications that still define any property under `grails.controllers.upload` fail at startup with migration instructions. +Move those settings to Spring Boot's `spring.servlet.multipart.*` namespace: + +[source,yaml] +.application.yml - Before (Grails 7) +---- +grails: + controllers: + upload: + maxFileSize: 200MB + maxRequestSize: 200MB +---- + +[source,yaml] +.application.yml - After (Grails 8) +---- +spring: + servlet: + multipart: + max-file-size: 200MB + max-request-size: 200MB +---- + +Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.file-size-threshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. + ==== 5. Hibernate ORM Package Relocations Spring Framework 7 removed the `org.springframework.orm.hibernate5` package entirely. From 2f9e4e534980f0fbf65cc1b32c7deedcfb5b4b0d Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Thu, 23 Jul 2026 13:27:28 +0200 Subject: [PATCH 05/22] Cleanups: Update legacy maxFileSize --- grails-doc/src/en/guide/upgrading/upgrading80x.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 4fefb929780..bac3b4f96f6 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -150,8 +150,8 @@ Move those settings to Spring Boot's `spring.servlet.multipart.*` namespace: grails: controllers: upload: - maxFileSize: 200MB - maxRequestSize: 200MB + maxFileSize: 200000 + maxRequestSize: 200000 ---- [source,yaml] From a360262fa71ecf38c96421b7ef0a4ad44c6b8f64 Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Mon, 27 Jul 2026 09:41:35 +0200 Subject: [PATCH 06/22] Reintroduced camelCase for multipart properties to keep consistency with Grails config --- THREAT_MODEL.md | 2 +- .../web/controllers/ControllersAutoConfiguration.java | 4 ++-- .../ControllersAutoConfigurationSpec.groovy | 4 ++-- .../guide/theWebLayer/controllers/uploadingFiles.adoc | 10 +++++----- grails-doc/src/en/guide/upgrading/upgrading80x.adoc | 6 +++--- threat-model.yaml | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index bb098481cf7..05234244125 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -191,7 +191,7 @@ The framework exposes a small number of configuration knobs whose value affects | `grails.databinding.autoGrowCollectionLimit` | 256 *(documented: [`SimpleDataBinder.groovy`](./grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy))* | Caps automatic collection growth during data binding - hard limit on memory amplification from an attacker submitting deeply indexed parameters (`list[1000000]=x`). Raising removes the cap. | **§14 wave 2** - is the documented default the supported production posture, or is the operator expected to lower it? | | `grails.databinding.dateFormats` / `dateParsingLenient` | RFC-3339 + locale defaults; lenient parsing on | Affects how strict date binding is. Loose parsing has historically been a source of validation-bypass findings in other frameworks. | **§14 wave 2** | | `grails.views.default.codec` and codec defaults (`grails.views.gsp.codecs.expression`, `scriptlet`, `taglib`, `staticparts`) | `html` for expression / scriptlet contexts (XSS protection on by default) | Setting any of these to `none` **disables automatic output encoding** for that context - immediate `OUT-OF-MODEL: non-default-build` for XSS reports under non-default settings. *(documented: [xssPrevention.adoc](./grails-doc/src/en/guide/security/xssPrevention.adoc), [codecs.adoc](./grails-doc/src/en/guide/security/codecs.adoc))* | **§14 wave 1** - confirm the `html` default is the supported production posture. | -| `spring.servlet.multipart.max-file-size` / `max-request-size` | **1 MB per file / 10 MB per request**, provided by Spring Boot's `MultipartProperties` defaults. *(documented: [uploadingFiles.adoc](./grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc))* | Multipart upload size cap. Operators who raise these past their application's actual need expose themselves to DoS via large multipart bodies. | **§14 wave 2** | +| `spring.servlet.multipart.maxFileSize` / `maxRequestSize` | **1 MB per file / 10 MB per request**, provided by Spring Boot's `MultipartProperties` defaults. *(documented: [uploadingFiles.adoc](./grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc))* | Multipart upload size cap. Operators who raise these past their application's actual need expose themselves to DoS via large multipart bodies. | **§14 wave 2** | | `grails.allowedMethods` (per-controller) | None (developer opt-in) | Restricts HTTP methods accepted by each action. Absence is **not** a finding; the model treats per-action method gating as a developer responsibility. *(inferred)* | **§14 wave 1** | | `grails.config.locations` (env var, system property, or config) | Empty | Adds external config file paths. **A non-empty value sourced from an untrusted location is a `BY-DESIGN: property-disclaimed` triage outcome** - see §9. | **§14 wave 1** - confirm this disposition. | | `GRAILS_ENV` / `grails.env` | `development` from CLI, `production` for assembled bootJars | Selects the active environment block in `application.yml` / `application.groovy`. Operators who deploy with `GRAILS_ENV=development` inherit the looser dev defaults (e.g., stack traces in responses). | **§14 wave 1** - is deploying with `development` a `non-default-build` posture? | diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 98f1fc7feeb..f1cb5a4c713 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -70,8 +70,8 @@ public class ControllersAutoConfiguration implements EnvironmentAware { private static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + - "'spring.servlet.multipart.max-file-size=200MB' and " + - "'spring.servlet.multipart.max-request-size=200MB'."; + "'spring.servlet.multipart.maxFileSize=200MB' and " + + "'spring.servlet.multipart.maxRequestSize=200MB'."; @Value("${" + Settings.FILTER_ENCODING + ":utf-8}") private String filtersEncoding; diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index 09f22fc8c8c..459b90b6eb7 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -75,8 +75,8 @@ class ControllersAutoConfigurationSpec extends Specification { exception.rootCause.message == "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + - "'spring.servlet.multipart.max-file-size=200MB' and " + - "'spring.servlet.multipart.max-request-size=200MB'." + "'spring.servlet.multipart.maxFileSize=200MB' and " + + "'spring.servlet.multipart.maxRequestSize=200MB'." cleanup: applicationContext.close() diff --git a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc index 2d0bf980690..efc930f88dd 100644 --- a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc +++ b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc @@ -106,15 +106,15 @@ You can configure the limit in your `application.yml` as follows: spring: servlet: multipart: - max-file-size: 200MB - max-request-size: 200MB + maxFileSize: 200MB + maxRequestSize: 200MB ---- -`max-file-size` = The maximum size allowed for an uploaded file. +`maxFileSize` = The maximum size allowed for an uploaded file. -`max-request-size` = The maximum size allowed for a multipart/form-data request. +`maxRequestSize` = The maximum size allowed for a multipart/form-data request. -The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `file-size-threshold`, and `enabled` properties. +The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `fileSizeThreshold`, and `enabled` properties. The former `grails.controllers.upload` configuration is no longer supported. Applications using it fail at startup with a message explaining how to migrate to `spring.servlet.multipart`. You should keep in mind https://www.owasp.org/index.php/Unrestricted_File_Upload[OWASP recommendations - Unrestricted File Upload] diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index bac3b4f96f6..ab4e1cfb3a8 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -160,11 +160,11 @@ grails: spring: servlet: multipart: - max-file-size: 200MB - max-request-size: 200MB + maxFileSize: 200MB + maxRequestSize: 200MB ---- -Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.file-size-threshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. +Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.fileSizeThreshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. ==== 5. Hibernate ORM Package Relocations diff --git a/threat-model.yaml b/threat-model.yaml index 8a3faa414f3..44463a8fb0a 100644 --- a/threat-model.yaml +++ b/threat-model.yaml @@ -124,14 +124,14 @@ config_knobs: security_relevant: true maintainer_stance: unresolved section: "§5a" - - name: spring.servlet.multipart.max-file-size + - name: spring.servlet.multipart.maxFileSize default: 1048576 default_units: bytes default_source: Spring Boot MultipartProperties security_relevant: true maintainer_stance: unresolved section: "§5a" - - name: spring.servlet.multipart.max-request-size + - name: spring.servlet.multipart.maxRequestSize default: 10485760 default_units: bytes default_source: Spring Boot MultipartProperties From 8b634512ac410f5a66966312077b68642d74fcce Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Wed, 22 Jul 2026 19:34:45 +0200 Subject: [PATCH 07/22] Implemented @matrei feedback --- .../web/controllers/ControllersAutoConfiguration.java | 2 +- .../controllers/ControllersAutoConfigurationSpec.groovy | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 8bb61519748..98f1fc7feeb 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -67,7 +67,7 @@ public class ControllersAutoConfiguration implements EnvironmentAware { private static final String LEGACY_MULTIPART_CONFIGURATION = "grails.controllers.upload"; - static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = + private static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + "'spring.servlet.multipart.max-file-size=200MB' and " + diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index fb90f00b612..09f22fc8c8c 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -70,9 +70,13 @@ class ControllersAutoConfigurationSpec extends Specification { applicationContext.refresh() then: - BeanCreationException exception = thrown() + def exception = thrown(BeanCreationException) exception.rootCause instanceof IllegalStateException - exception.rootCause.message == ControllersAutoConfiguration.LEGACY_MULTIPART_CONFIGURATION_ERROR + exception.rootCause.message == + "Configuration properties under 'grails.controllers.upload' are no longer supported. " + + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + + "'spring.servlet.multipart.max-file-size=200MB' and " + + "'spring.servlet.multipart.max-request-size=200MB'." cleanup: applicationContext.close() From 984e7c8abfefdd2065c6a50568c230a5776cb560 Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Thu, 23 Jul 2026 11:08:15 +0200 Subject: [PATCH 08/22] Upgrade guide --- .../src/en/guide/upgrading/upgrading80x.adoc | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 2bb87483681..4fefb929780 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -109,7 +109,9 @@ org.springframework.boot.env.EnvironmentPostProcessor=com.example.MyPostProcesso org.springframework.boot.EnvironmentPostProcessor=com.example.MyPostProcessor ---- -==== 4. MongoDB Configuration Property Changes +==== 4. Spring Boot Configuration Property Changes + +===== 4.1 MongoDB Configuration Spring Boot 4 renamed the MongoDB configuration property namespace for Spring Boot's own auto-configuration. If your `application.yml` or `application.groovy` uses `spring.data.mongodb.*` properties for Spring Boot auto-configuration, you must rename them to `spring.mongodb.*`. @@ -136,6 +138,34 @@ spring: database: mydb ---- +===== 4.2 Multipart Upload Configuration + +The `grails.controllers.upload.*` configuration properties are no longer supported. +Applications that still define any property under `grails.controllers.upload` fail at startup with migration instructions. +Move those settings to Spring Boot's `spring.servlet.multipart.*` namespace: + +[source,yaml] +.application.yml - Before (Grails 7) +---- +grails: + controllers: + upload: + maxFileSize: 200MB + maxRequestSize: 200MB +---- + +[source,yaml] +.application.yml - After (Grails 8) +---- +spring: + servlet: + multipart: + max-file-size: 200MB + max-request-size: 200MB +---- + +Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.file-size-threshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. + ==== 5. Hibernate ORM Package Relocations Spring Framework 7 removed the `org.springframework.orm.hibernate5` package entirely. From 5e90de8495b25d95cdafdc805a216719dea1fe88 Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Thu, 23 Jul 2026 13:27:28 +0200 Subject: [PATCH 09/22] Cleanups: Update legacy maxFileSize --- grails-doc/src/en/guide/upgrading/upgrading80x.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 4fefb929780..bac3b4f96f6 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -150,8 +150,8 @@ Move those settings to Spring Boot's `spring.servlet.multipart.*` namespace: grails: controllers: upload: - maxFileSize: 200MB - maxRequestSize: 200MB + maxFileSize: 200000 + maxRequestSize: 200000 ---- [source,yaml] From 43c1b91cddc3dda2a0472675206da785eb5aa07b Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Mon, 27 Jul 2026 09:41:35 +0200 Subject: [PATCH 10/22] Reintroduced camelCase for multipart properties to keep consistency with Grails config --- THREAT_MODEL.md | 2 +- .../web/controllers/ControllersAutoConfiguration.java | 4 ++-- .../ControllersAutoConfigurationSpec.groovy | 4 ++-- .../guide/theWebLayer/controllers/uploadingFiles.adoc | 10 +++++----- grails-doc/src/en/guide/upgrading/upgrading80x.adoc | 6 +++--- threat-model.yaml | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index bb098481cf7..05234244125 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -191,7 +191,7 @@ The framework exposes a small number of configuration knobs whose value affects | `grails.databinding.autoGrowCollectionLimit` | 256 *(documented: [`SimpleDataBinder.groovy`](./grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy))* | Caps automatic collection growth during data binding - hard limit on memory amplification from an attacker submitting deeply indexed parameters (`list[1000000]=x`). Raising removes the cap. | **§14 wave 2** - is the documented default the supported production posture, or is the operator expected to lower it? | | `grails.databinding.dateFormats` / `dateParsingLenient` | RFC-3339 + locale defaults; lenient parsing on | Affects how strict date binding is. Loose parsing has historically been a source of validation-bypass findings in other frameworks. | **§14 wave 2** | | `grails.views.default.codec` and codec defaults (`grails.views.gsp.codecs.expression`, `scriptlet`, `taglib`, `staticparts`) | `html` for expression / scriptlet contexts (XSS protection on by default) | Setting any of these to `none` **disables automatic output encoding** for that context - immediate `OUT-OF-MODEL: non-default-build` for XSS reports under non-default settings. *(documented: [xssPrevention.adoc](./grails-doc/src/en/guide/security/xssPrevention.adoc), [codecs.adoc](./grails-doc/src/en/guide/security/codecs.adoc))* | **§14 wave 1** - confirm the `html` default is the supported production posture. | -| `spring.servlet.multipart.max-file-size` / `max-request-size` | **1 MB per file / 10 MB per request**, provided by Spring Boot's `MultipartProperties` defaults. *(documented: [uploadingFiles.adoc](./grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc))* | Multipart upload size cap. Operators who raise these past their application's actual need expose themselves to DoS via large multipart bodies. | **§14 wave 2** | +| `spring.servlet.multipart.maxFileSize` / `maxRequestSize` | **1 MB per file / 10 MB per request**, provided by Spring Boot's `MultipartProperties` defaults. *(documented: [uploadingFiles.adoc](./grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc))* | Multipart upload size cap. Operators who raise these past their application's actual need expose themselves to DoS via large multipart bodies. | **§14 wave 2** | | `grails.allowedMethods` (per-controller) | None (developer opt-in) | Restricts HTTP methods accepted by each action. Absence is **not** a finding; the model treats per-action method gating as a developer responsibility. *(inferred)* | **§14 wave 1** | | `grails.config.locations` (env var, system property, or config) | Empty | Adds external config file paths. **A non-empty value sourced from an untrusted location is a `BY-DESIGN: property-disclaimed` triage outcome** - see §9. | **§14 wave 1** - confirm this disposition. | | `GRAILS_ENV` / `grails.env` | `development` from CLI, `production` for assembled bootJars | Selects the active environment block in `application.yml` / `application.groovy`. Operators who deploy with `GRAILS_ENV=development` inherit the looser dev defaults (e.g., stack traces in responses). | **§14 wave 1** - is deploying with `development` a `non-default-build` posture? | diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 98f1fc7feeb..f1cb5a4c713 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -70,8 +70,8 @@ public class ControllersAutoConfiguration implements EnvironmentAware { private static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + - "'spring.servlet.multipart.max-file-size=200MB' and " + - "'spring.servlet.multipart.max-request-size=200MB'."; + "'spring.servlet.multipart.maxFileSize=200MB' and " + + "'spring.servlet.multipart.maxRequestSize=200MB'."; @Value("${" + Settings.FILTER_ENCODING + ":utf-8}") private String filtersEncoding; diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index 09f22fc8c8c..459b90b6eb7 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -75,8 +75,8 @@ class ControllersAutoConfigurationSpec extends Specification { exception.rootCause.message == "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + - "'spring.servlet.multipart.max-file-size=200MB' and " + - "'spring.servlet.multipart.max-request-size=200MB'." + "'spring.servlet.multipart.maxFileSize=200MB' and " + + "'spring.servlet.multipart.maxRequestSize=200MB'." cleanup: applicationContext.close() diff --git a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc index 2d0bf980690..efc930f88dd 100644 --- a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc +++ b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc @@ -106,15 +106,15 @@ You can configure the limit in your `application.yml` as follows: spring: servlet: multipart: - max-file-size: 200MB - max-request-size: 200MB + maxFileSize: 200MB + maxRequestSize: 200MB ---- -`max-file-size` = The maximum size allowed for an uploaded file. +`maxFileSize` = The maximum size allowed for an uploaded file. -`max-request-size` = The maximum size allowed for a multipart/form-data request. +`maxRequestSize` = The maximum size allowed for a multipart/form-data request. -The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `file-size-threshold`, and `enabled` properties. +The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `fileSizeThreshold`, and `enabled` properties. The former `grails.controllers.upload` configuration is no longer supported. Applications using it fail at startup with a message explaining how to migrate to `spring.servlet.multipart`. You should keep in mind https://www.owasp.org/index.php/Unrestricted_File_Upload[OWASP recommendations - Unrestricted File Upload] diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index bac3b4f96f6..ab4e1cfb3a8 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -160,11 +160,11 @@ grails: spring: servlet: multipart: - max-file-size: 200MB - max-request-size: 200MB + maxFileSize: 200MB + maxRequestSize: 200MB ---- -Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.file-size-threshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. +Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.fileSizeThreshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. ==== 5. Hibernate ORM Package Relocations diff --git a/threat-model.yaml b/threat-model.yaml index 8a3faa414f3..44463a8fb0a 100644 --- a/threat-model.yaml +++ b/threat-model.yaml @@ -124,14 +124,14 @@ config_knobs: security_relevant: true maintainer_stance: unresolved section: "§5a" - - name: spring.servlet.multipart.max-file-size + - name: spring.servlet.multipart.maxFileSize default: 1048576 default_units: bytes default_source: Spring Boot MultipartProperties security_relevant: true maintainer_stance: unresolved section: "§5a" - - name: spring.servlet.multipart.max-request-size + - name: spring.servlet.multipart.maxRequestSize default: 10485760 default_units: bytes default_source: Spring Boot MultipartProperties From 925ffc845e9e9c8301298aa041c6ea61140fbf4d Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Wed, 22 Jul 2026 19:34:45 +0200 Subject: [PATCH 11/22] Implemented @matrei feedback --- .../web/controllers/ControllersAutoConfiguration.java | 2 +- .../controllers/ControllersAutoConfigurationSpec.groovy | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 8bb61519748..98f1fc7feeb 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -67,7 +67,7 @@ public class ControllersAutoConfiguration implements EnvironmentAware { private static final String LEGACY_MULTIPART_CONFIGURATION = "grails.controllers.upload"; - static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = + private static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + "'spring.servlet.multipart.max-file-size=200MB' and " + diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index fb90f00b612..09f22fc8c8c 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -70,9 +70,13 @@ class ControllersAutoConfigurationSpec extends Specification { applicationContext.refresh() then: - BeanCreationException exception = thrown() + def exception = thrown(BeanCreationException) exception.rootCause instanceof IllegalStateException - exception.rootCause.message == ControllersAutoConfiguration.LEGACY_MULTIPART_CONFIGURATION_ERROR + exception.rootCause.message == + "Configuration properties under 'grails.controllers.upload' are no longer supported. " + + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + + "'spring.servlet.multipart.max-file-size=200MB' and " + + "'spring.servlet.multipart.max-request-size=200MB'." cleanup: applicationContext.close() From c14c0d9707279c161fac5104c7e170aa0f136d48 Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Thu, 23 Jul 2026 11:08:15 +0200 Subject: [PATCH 12/22] Upgrade guide --- .../src/en/guide/upgrading/upgrading80x.adoc | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 2bb87483681..4fefb929780 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -109,7 +109,9 @@ org.springframework.boot.env.EnvironmentPostProcessor=com.example.MyPostProcesso org.springframework.boot.EnvironmentPostProcessor=com.example.MyPostProcessor ---- -==== 4. MongoDB Configuration Property Changes +==== 4. Spring Boot Configuration Property Changes + +===== 4.1 MongoDB Configuration Spring Boot 4 renamed the MongoDB configuration property namespace for Spring Boot's own auto-configuration. If your `application.yml` or `application.groovy` uses `spring.data.mongodb.*` properties for Spring Boot auto-configuration, you must rename them to `spring.mongodb.*`. @@ -136,6 +138,34 @@ spring: database: mydb ---- +===== 4.2 Multipart Upload Configuration + +The `grails.controllers.upload.*` configuration properties are no longer supported. +Applications that still define any property under `grails.controllers.upload` fail at startup with migration instructions. +Move those settings to Spring Boot's `spring.servlet.multipart.*` namespace: + +[source,yaml] +.application.yml - Before (Grails 7) +---- +grails: + controllers: + upload: + maxFileSize: 200MB + maxRequestSize: 200MB +---- + +[source,yaml] +.application.yml - After (Grails 8) +---- +spring: + servlet: + multipart: + max-file-size: 200MB + max-request-size: 200MB +---- + +Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.file-size-threshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. + ==== 5. Hibernate ORM Package Relocations Spring Framework 7 removed the `org.springframework.orm.hibernate5` package entirely. From ab0c2befcba3f079cc59a097d49959454705910a Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Thu, 23 Jul 2026 13:27:28 +0200 Subject: [PATCH 13/22] Cleanups: Update legacy maxFileSize --- grails-doc/src/en/guide/upgrading/upgrading80x.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 4fefb929780..bac3b4f96f6 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -150,8 +150,8 @@ Move those settings to Spring Boot's `spring.servlet.multipart.*` namespace: grails: controllers: upload: - maxFileSize: 200MB - maxRequestSize: 200MB + maxFileSize: 200000 + maxRequestSize: 200000 ---- [source,yaml] From 1de6ba4739435b2d219d01ef9580f5d1bee33e40 Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Mon, 27 Jul 2026 09:41:35 +0200 Subject: [PATCH 14/22] Reintroduced camelCase for multipart properties to keep consistency with Grails config --- THREAT_MODEL.md | 2 +- .../web/controllers/ControllersAutoConfiguration.java | 4 ++-- .../ControllersAutoConfigurationSpec.groovy | 4 ++-- .../guide/theWebLayer/controllers/uploadingFiles.adoc | 10 +++++----- grails-doc/src/en/guide/upgrading/upgrading80x.adoc | 6 +++--- threat-model.yaml | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index bb098481cf7..05234244125 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -191,7 +191,7 @@ The framework exposes a small number of configuration knobs whose value affects | `grails.databinding.autoGrowCollectionLimit` | 256 *(documented: [`SimpleDataBinder.groovy`](./grails-databinding-core/src/main/groovy/grails/databinding/SimpleDataBinder.groovy))* | Caps automatic collection growth during data binding - hard limit on memory amplification from an attacker submitting deeply indexed parameters (`list[1000000]=x`). Raising removes the cap. | **§14 wave 2** - is the documented default the supported production posture, or is the operator expected to lower it? | | `grails.databinding.dateFormats` / `dateParsingLenient` | RFC-3339 + locale defaults; lenient parsing on | Affects how strict date binding is. Loose parsing has historically been a source of validation-bypass findings in other frameworks. | **§14 wave 2** | | `grails.views.default.codec` and codec defaults (`grails.views.gsp.codecs.expression`, `scriptlet`, `taglib`, `staticparts`) | `html` for expression / scriptlet contexts (XSS protection on by default) | Setting any of these to `none` **disables automatic output encoding** for that context - immediate `OUT-OF-MODEL: non-default-build` for XSS reports under non-default settings. *(documented: [xssPrevention.adoc](./grails-doc/src/en/guide/security/xssPrevention.adoc), [codecs.adoc](./grails-doc/src/en/guide/security/codecs.adoc))* | **§14 wave 1** - confirm the `html` default is the supported production posture. | -| `spring.servlet.multipart.max-file-size` / `max-request-size` | **1 MB per file / 10 MB per request**, provided by Spring Boot's `MultipartProperties` defaults. *(documented: [uploadingFiles.adoc](./grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc))* | Multipart upload size cap. Operators who raise these past their application's actual need expose themselves to DoS via large multipart bodies. | **§14 wave 2** | +| `spring.servlet.multipart.maxFileSize` / `maxRequestSize` | **1 MB per file / 10 MB per request**, provided by Spring Boot's `MultipartProperties` defaults. *(documented: [uploadingFiles.adoc](./grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc))* | Multipart upload size cap. Operators who raise these past their application's actual need expose themselves to DoS via large multipart bodies. | **§14 wave 2** | | `grails.allowedMethods` (per-controller) | None (developer opt-in) | Restricts HTTP methods accepted by each action. Absence is **not** a finding; the model treats per-action method gating as a developer responsibility. *(inferred)* | **§14 wave 1** | | `grails.config.locations` (env var, system property, or config) | Empty | Adds external config file paths. **A non-empty value sourced from an untrusted location is a `BY-DESIGN: property-disclaimed` triage outcome** - see §9. | **§14 wave 1** - confirm this disposition. | | `GRAILS_ENV` / `grails.env` | `development` from CLI, `production` for assembled bootJars | Selects the active environment block in `application.yml` / `application.groovy`. Operators who deploy with `GRAILS_ENV=development` inherit the looser dev defaults (e.g., stack traces in responses). | **§14 wave 1** - is deploying with `development` a `non-default-build` posture? | diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 98f1fc7feeb..f1cb5a4c713 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -70,8 +70,8 @@ public class ControllersAutoConfiguration implements EnvironmentAware { private static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + - "'spring.servlet.multipart.max-file-size=200MB' and " + - "'spring.servlet.multipart.max-request-size=200MB'."; + "'spring.servlet.multipart.maxFileSize=200MB' and " + + "'spring.servlet.multipart.maxRequestSize=200MB'."; @Value("${" + Settings.FILTER_ENCODING + ":utf-8}") private String filtersEncoding; diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index 09f22fc8c8c..459b90b6eb7 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -75,8 +75,8 @@ class ControllersAutoConfigurationSpec extends Specification { exception.rootCause.message == "Configuration properties under 'grails.controllers.upload' are no longer supported. " + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + - "'spring.servlet.multipart.max-file-size=200MB' and " + - "'spring.servlet.multipart.max-request-size=200MB'." + "'spring.servlet.multipart.maxFileSize=200MB' and " + + "'spring.servlet.multipart.maxRequestSize=200MB'." cleanup: applicationContext.close() diff --git a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc index 2d0bf980690..efc930f88dd 100644 --- a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc +++ b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc @@ -106,15 +106,15 @@ You can configure the limit in your `application.yml` as follows: spring: servlet: multipart: - max-file-size: 200MB - max-request-size: 200MB + maxFileSize: 200MB + maxRequestSize: 200MB ---- -`max-file-size` = The maximum size allowed for an uploaded file. +`maxFileSize` = The maximum size allowed for an uploaded file. -`max-request-size` = The maximum size allowed for a multipart/form-data request. +`maxRequestSize` = The maximum size allowed for a multipart/form-data request. -The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `file-size-threshold`, and `enabled` properties. +The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `fileSizeThreshold`, and `enabled` properties. The former `grails.controllers.upload` configuration is no longer supported. Applications using it fail at startup with a message explaining how to migrate to `spring.servlet.multipart`. You should keep in mind https://www.owasp.org/index.php/Unrestricted_File_Upload[OWASP recommendations - Unrestricted File Upload] diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index bac3b4f96f6..ab4e1cfb3a8 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -160,11 +160,11 @@ grails: spring: servlet: multipart: - max-file-size: 200MB - max-request-size: 200MB + maxFileSize: 200MB + maxRequestSize: 200MB ---- -Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.file-size-threshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. +Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.fileSizeThreshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. ==== 5. Hibernate ORM Package Relocations diff --git a/threat-model.yaml b/threat-model.yaml index 8a3faa414f3..44463a8fb0a 100644 --- a/threat-model.yaml +++ b/threat-model.yaml @@ -124,14 +124,14 @@ config_knobs: security_relevant: true maintainer_stance: unresolved section: "§5a" - - name: spring.servlet.multipart.max-file-size + - name: spring.servlet.multipart.maxFileSize default: 1048576 default_units: bytes default_source: Spring Boot MultipartProperties security_relevant: true maintainer_stance: unresolved section: "§5a" - - name: spring.servlet.multipart.max-request-size + - name: spring.servlet.multipart.maxRequestSize default: 10485760 default_units: bytes default_source: Spring Boot MultipartProperties From cf2e542e50c5bc6a5000ee7683162835797b3764 Mon Sep 17 00:00:00 2001 From: "g.sartori" Date: Wed, 5 Aug 2026 10:17:20 +0200 Subject: [PATCH 15/22] Fix full stack trace output when System.err changes --- .../reporting/DefaultStackTraceFilterer.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/grails-bootstrap/src/main/groovy/org/grails/exceptions/reporting/DefaultStackTraceFilterer.java b/grails-bootstrap/src/main/groovy/org/grails/exceptions/reporting/DefaultStackTraceFilterer.java index f1c9aa7caae..eabe3dcb797 100644 --- a/grails-bootstrap/src/main/groovy/org/grails/exceptions/reporting/DefaultStackTraceFilterer.java +++ b/grails-bootstrap/src/main/groovy/org/grails/exceptions/reporting/DefaultStackTraceFilterer.java @@ -18,6 +18,7 @@ */ package org.grails.exceptions.reporting; +import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -35,9 +36,11 @@ public class DefaultStackTraceFilterer implements StackTraceFilterer { public static final String STACK_LOG_NAME = "StackTrace"; /** * Dedicated logger for exception stack traces. The filterer emits the unfiltered - * trace to this logger as a side effect of {@link #filter(Throwable)} — before the - * trace is trimmed in place — when {@link #logFullStackTraceOnFilter} is {@code true} - * (the default). {@code GrailsExceptionResolver} also emits to this logger when + * trace to this logger and {@link System#err} as a side effect of {@link #filter(Throwable)} — + * before the trace is trimmed in place — when {@link #logFullStackTraceOnFilter} is {@code true} + * (the default). Emitting to the current error stream ensures the trace is not lost when a + * logging backend has no provider or retains an earlier stream reference. + * {@code GrailsExceptionResolver} also emits to this logger when * {@code grails.exceptionresolver.logFullStackTrace} is enabled. Exposed as a public * constant so that subclasses and logback configurations can reference the logger * name symbolically. @@ -89,7 +92,7 @@ public void setCutOffPackage(String cutOffPackage) { /** * Controls whether {@link #filter(Throwable)} emits the unfiltered stack trace - * to {@link #STACK_LOG} as a side effect before trimming the trace in place. + * to {@link #STACK_LOG} and {@link System#err} as a side effect before trimming the trace in place. * Defaults to {@code true} for backwards compatibility with pre-7.1 behaviour; * set to {@code false} to disable the side-effect emission. The exception * resolver wires this from {@code grails.exceptionresolver.logFullStackTraceOnFilter}. @@ -129,6 +132,9 @@ public Throwable filter(Throwable source) { // emit the unfiltered trace before mutating in place; once setStackTrace(clean) // runs the original frames are gone STACK_LOG.error(FULL_STACK_TRACE_MESSAGE, source); + PrintStream errorStream = System.err; + errorStream.println(FULL_STACK_TRACE_MESSAGE); + source.printStackTrace(errorStream); } StackTraceElement[] clean = new StackTraceElement[newTrace.size()]; newTrace.toArray(clean); From 3941a0cec942d4088692a84a62d54db73f3119b9 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Wed, 5 Aug 2026 11:31:33 +0200 Subject: [PATCH 16/22] Revert "Fix full stack trace output when System.err changes" This reverts commit cf2e542e50c5bc6a5000ee7683162835797b3764. --- .../reporting/DefaultStackTraceFilterer.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/grails-bootstrap/src/main/groovy/org/grails/exceptions/reporting/DefaultStackTraceFilterer.java b/grails-bootstrap/src/main/groovy/org/grails/exceptions/reporting/DefaultStackTraceFilterer.java index eabe3dcb797..f1c9aa7caae 100644 --- a/grails-bootstrap/src/main/groovy/org/grails/exceptions/reporting/DefaultStackTraceFilterer.java +++ b/grails-bootstrap/src/main/groovy/org/grails/exceptions/reporting/DefaultStackTraceFilterer.java @@ -18,7 +18,6 @@ */ package org.grails.exceptions.reporting; -import java.io.PrintStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -36,11 +35,9 @@ public class DefaultStackTraceFilterer implements StackTraceFilterer { public static final String STACK_LOG_NAME = "StackTrace"; /** * Dedicated logger for exception stack traces. The filterer emits the unfiltered - * trace to this logger and {@link System#err} as a side effect of {@link #filter(Throwable)} — - * before the trace is trimmed in place — when {@link #logFullStackTraceOnFilter} is {@code true} - * (the default). Emitting to the current error stream ensures the trace is not lost when a - * logging backend has no provider or retains an earlier stream reference. - * {@code GrailsExceptionResolver} also emits to this logger when + * trace to this logger as a side effect of {@link #filter(Throwable)} — before the + * trace is trimmed in place — when {@link #logFullStackTraceOnFilter} is {@code true} + * (the default). {@code GrailsExceptionResolver} also emits to this logger when * {@code grails.exceptionresolver.logFullStackTrace} is enabled. Exposed as a public * constant so that subclasses and logback configurations can reference the logger * name symbolically. @@ -92,7 +89,7 @@ public void setCutOffPackage(String cutOffPackage) { /** * Controls whether {@link #filter(Throwable)} emits the unfiltered stack trace - * to {@link #STACK_LOG} and {@link System#err} as a side effect before trimming the trace in place. + * to {@link #STACK_LOG} as a side effect before trimming the trace in place. * Defaults to {@code true} for backwards compatibility with pre-7.1 behaviour; * set to {@code false} to disable the side-effect emission. The exception * resolver wires this from {@code grails.exceptionresolver.logFullStackTraceOnFilter}. @@ -132,9 +129,6 @@ public Throwable filter(Throwable source) { // emit the unfiltered trace before mutating in place; once setStackTrace(clean) // runs the original frames are gone STACK_LOG.error(FULL_STACK_TRACE_MESSAGE, source); - PrintStream errorStream = System.err; - errorStream.println(FULL_STACK_TRACE_MESSAGE); - source.printStackTrace(errorStream); } StackTraceElement[] clean = new StackTraceElement[newTrace.size()]; newTrace.toArray(clean); From d62fd9c837cbdbc599a21e58df46ec5931f018cf Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 10 Aug 2026 15:46:11 +0200 Subject: [PATCH 17/22] test: add multipart configuration tests for dispatcher servlet registration --- .../ControllersAutoConfigurationSpec.groovy | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index 459b90b6eb7..fc6bd100968 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -26,10 +26,12 @@ import grails.core.GrailsApplication import org.springframework.beans.factory.BeanCreationException import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.servlet.autoconfigure.MultipartAutoConfiguration import org.springframework.boot.test.context.runner.WebApplicationContextRunner import org.springframework.boot.web.servlet.AbstractFilterRegistrationBean import org.springframework.boot.web.servlet.FilterRegistrationBean import org.springframework.boot.web.servlet.ServletContextInitializerBeans +import org.springframework.boot.webmvc.autoconfigure.DispatcherServletRegistrationBean import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration import org.springframework.context.ApplicationContext import org.springframework.context.ConfigurableApplicationContext @@ -153,6 +155,47 @@ class ControllersAutoConfigurationSpec extends Specification { } } + void 'Boot multipart configuration reaches the dispatcher servlet registration'() { + expect: + new WebApplicationContextRunner() + .withBean(GrailsApplication, grailsApplicationSupplier()) + .withPropertyValues('spring.servlet.multipart.maxFileSize=1MB') + .withConfiguration(AutoConfigurations.of( + ControllersAutoConfiguration, + WebMvcAutoConfiguration, + MultipartAutoConfiguration)) + .run { context -> + assert context.getBean(DispatcherServletRegistrationBean).multipartConfig.maxFileSize == 1024 * 1024 + } + } + + void 'disabled Boot multipart configuration is not added to the dispatcher servlet registration'() { + expect: + new WebApplicationContextRunner() + .withBean(GrailsApplication, grailsApplicationSupplier()) + .withPropertyValues('spring.servlet.multipart.enabled=false') + .withConfiguration(AutoConfigurations.of( + ControllersAutoConfiguration, + WebMvcAutoConfiguration, + MultipartAutoConfiguration)) + .run { context -> + assert context.getBean(DispatcherServletRegistrationBean).multipartConfig == null + } + } + + void 'the dispatcher servlet registration starts without the legacy multipart property'() { + expect: + new WebApplicationContextRunner() + .withBean(GrailsApplication, grailsApplicationSupplier()) + .withConfiguration(AutoConfigurations.of( + ControllersAutoConfiguration, + WebMvcAutoConfiguration, + MultipartAutoConfiguration)) + .run { context -> + assert context.startupFailure == null + } + } + void 'a user-defined GrailsWebMvcConfigurer bean makes the auto-configured webMvcConfig back off'() { given: 'a GrailsApplication, required by the controllers auto-config' def grailsApplication = Mock(GrailsApplication) { @@ -238,6 +281,12 @@ class ControllersAutoConfigurationSpec extends Specification { return servletContext } + private Supplier grailsApplicationSupplier() { + () -> Mock(GrailsApplication) { + getClassLoader() >> getClass().classLoader + } + } + // Reconstructs the servlet filter chain the way Boot assembles it at container start, so the specs // assert on the real chain — including any raw filter bean Boot auto-adapts onto "/*" — rather than // mere bean presence in the context. From 7488c877a5300380187867df7ff5ce7503263875 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 10 Aug 2026 15:57:06 +0200 Subject: [PATCH 18/22] test: simplify `ControllersAutoConfigurationSpec` --- .../ControllersAutoConfigurationSpec.groovy | 95 +++++++------------ 1 file changed, 35 insertions(+), 60 deletions(-) diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index fc6bd100968..f40f40f41b4 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -61,12 +61,13 @@ class ControllersAutoConfigurationSpec extends Specification { def "legacy multipart configuration fails startup with migration instructions"() { given: - def applicationContext = new AnnotationConfigWebApplicationContext() - applicationContext.servletContext = new MockServletContext() - applicationContext.environment.propertySources.addFirst(new MapPropertySource('test', [ - 'grails.controllers.upload.maxFileSize': 20000000, - ])) - applicationContext.register(ControllersAutoConfiguration) + def applicationContext = new AnnotationConfigWebApplicationContext().tap { + servletContext = new MockServletContext() + environment.propertySources.addFirst(new MapPropertySource('test', [ + 'grails.controllers.upload.maxFileSize': 20000000, + ])) + register(ControllersAutoConfiguration) + } when: applicationContext.refresh() @@ -86,7 +87,7 @@ class ControllersAutoConfigurationSpec extends Specification { void 'grailsWebRequest filter is a RequestContextFilter so Boot WebMvcAutoConfiguration backs off its own RequestContextFilter'() { when: 'the Grails request-binding filter bean is created' - GrailsWebRequestFilter filter = autoConfiguration.grailsWebRequest(applicationContext) + def filter = autoConfiguration.grailsWebRequest(applicationContext) then: 'it is exposed as a RequestContextFilter, the type Boot @ConditionalOnMissingBean keys on' filter != null @@ -95,10 +96,10 @@ class ControllersAutoConfigurationSpec extends Specification { void 'grailsWebRequestFilter registers the GrailsWebRequestFilter with the Grails request-filter order'() { given: 'the Grails request-binding filter' - GrailsWebRequestFilter filter = autoConfiguration.grailsWebRequest(applicationContext) + def filter = autoConfiguration.grailsWebRequest(applicationContext) when: 'it is wrapped in a registration bean' - FilterRegistrationBean registrationBean = autoConfiguration.grailsWebRequestFilter(filter) + def registrationBean = autoConfiguration.grailsWebRequestFilter(filter) then: 'the same filter instance is registered ahead of the Spring Security chain' registrationBean.filter.is(filter) @@ -115,15 +116,9 @@ class ControllersAutoConfigurationSpec extends Specification { } void 'Grails controllers auto-config makes Boot WebMvcAutoConfiguration back off its requestContextFilter'() { - given: 'a GrailsApplication, required by the controllers auto-config' - def grailsApplication = Mock(GrailsApplication) { - getClassLoader() >> getClass().classLoader - } - Supplier grailsApplicationSupplier = () -> grailsApplication - expect: 'Boot does not contribute its OrderedRequestContextFilter, leaving GrailsWebRequest bound' new WebApplicationContextRunner() - .withBean(GrailsApplication, grailsApplicationSupplier) + .withBean(GrailsApplication, grailsApplicationSupplier()) .withConfiguration(AutoConfigurations.of(ControllersAutoConfiguration, WebMvcAutoConfiguration)) .run { context -> assert !context.containsBean('requestContextFilter') @@ -133,19 +128,13 @@ class ControllersAutoConfigurationSpec extends Specification { } void 'a user-defined grailsWebRequestFilter registration bean makes the auto-configured one back off'() { - given: 'a GrailsApplication, required by the controllers auto-config' - def grailsApplication = Mock(GrailsApplication) { - getClassLoader() >> getClass().classLoader - } - Supplier grailsApplicationSupplier = () -> grailsApplication - - and: 'a user-defined registration bean under the auto-configured bean name' - FilterRegistrationBean userRegistration = new FilterRegistrationBean<>() - Supplier userRegistrationSupplier = () -> userRegistration + given: 'a user-defined registration bean under the auto-configured bean name' + def userRegistration = new FilterRegistrationBean<>() + def userRegistrationSupplier = () -> userRegistration expect: 'the user bean wins and the framework filter backs off entirely — no second, Boot-adapted copy on the chain' new WebApplicationContextRunner() - .withBean(GrailsApplication, grailsApplicationSupplier) + .withBean(GrailsApplication, grailsApplicationSupplier()) .withBean('grailsWebRequestFilter', FilterRegistrationBean, userRegistrationSupplier) .withConfiguration(AutoConfigurations.of(ControllersAutoConfiguration, WebMvcAutoConfiguration)) .run { context -> @@ -197,19 +186,13 @@ class ControllersAutoConfigurationSpec extends Specification { } void 'a user-defined GrailsWebMvcConfigurer bean makes the auto-configured webMvcConfig back off'() { - given: 'a GrailsApplication, required by the controllers auto-config' - def grailsApplication = Mock(GrailsApplication) { - getClassLoader() >> getClass().classLoader - } - Supplier grailsApplicationSupplier = () -> grailsApplication - - and: 'a user-defined web MVC configurer' + given: 'a user-defined web MVC configurer' def userConfigurer = new ControllersAutoConfiguration.GrailsWebMvcConfigurer(0, false, '/custom/**') - Supplier userConfigurerSupplier = () -> userConfigurer + def userConfigurerSupplier = () -> userConfigurer expect: 'the user bean wins and only one GrailsWebMvcConfigurer exists' new WebApplicationContextRunner() - .withBean(GrailsApplication, grailsApplicationSupplier) + .withBean(GrailsApplication, grailsApplicationSupplier()) .withBean(ControllersAutoConfiguration.GrailsWebMvcConfigurer, userConfigurerSupplier) .withConfiguration(AutoConfigurations.of(ControllersAutoConfiguration, WebMvcAutoConfiguration)) .run { context -> @@ -221,9 +204,10 @@ class ControllersAutoConfigurationSpec extends Specification { void 'the default exceptionHandler maps exceptions to the error view'() { given: 'the auto-configured exception resolver, wired the way the runtime does' - GrailsExceptionResolver exceptionResolver = autoConfiguration.exceptionHandler() - exceptionResolver.grailsApplication = new DefaultGrailsApplication() - exceptionResolver.servletContext = servletContextWithWebApplicationContext() + def exceptionResolver = autoConfiguration.exceptionHandler().tap { + grailsApplication = new DefaultGrailsApplication() + servletContext = servletContextWithWebApplicationContext() + } when: def modelAndView = exceptionResolver.resolveException( @@ -234,15 +218,9 @@ class ControllersAutoConfigurationSpec extends Specification { } void 'the exceptionHandler default is auto-configured when no user bean exists'() { - given: 'a GrailsApplication, required by the controllers auto-config' - def grailsApplication = Mock(GrailsApplication) { - getClassLoader() >> getClass().classLoader - } - Supplier grailsApplicationSupplier = () -> grailsApplication - expect: new WebApplicationContextRunner() - .withBean(GrailsApplication, grailsApplicationSupplier) + .withBean(GrailsApplication, grailsApplicationSupplier()) .withConfiguration(AutoConfigurations.of(ControllersAutoConfiguration, WebMvcAutoConfiguration)) .run { context -> assert context.getBean('exceptionHandler') instanceof GrailsExceptionResolver @@ -250,20 +228,14 @@ class ControllersAutoConfigurationSpec extends Specification { } void 'a user-defined exceptionHandler bean makes the auto-configured default back off'() { - given: 'a GrailsApplication, required by the controllers auto-config' - def grailsApplication = Mock(GrailsApplication) { - getClassLoader() >> getClass().classLoader - } - Supplier grailsApplicationSupplier = () -> grailsApplication - - and: 'a user-defined exception resolver under the auto-configured bean name' + given: 'a user-defined exception resolver under the auto-configured bean name' def userResolver = new SimpleMappingExceptionResolver() - Supplier userResolverSupplier = () -> userResolver + def userResolverSupplier = () -> userResolver expect: 'the user bean wins and the framework default is never registered' new WebApplicationContextRunner() .withBean('exceptionHandler', SimpleMappingExceptionResolver, userResolverSupplier) - .withBean(GrailsApplication, grailsApplicationSupplier) + .withBean(GrailsApplication, grailsApplicationSupplier()) .withConfiguration(AutoConfigurations.of(ControllersAutoConfiguration, WebMvcAutoConfiguration)) .run { context -> assert context.getBean('exceptionHandler').is(userResolver) @@ -273,12 +245,15 @@ class ControllersAutoConfigurationSpec extends Specification { private static MockServletContext servletContextWithWebApplicationContext() { def servletContext = new MockServletContext() - def webApplicationContext = new StaticWebApplicationContext() - webApplicationContext.servletContext = servletContext - webApplicationContext.refresh() - webApplicationContext.beanFactory.registerSingleton(GrailsApplication.APPLICATION_ID, new DefaultGrailsApplication()) - servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, webApplicationContext) - return servletContext + servletContext.setAttribute( + WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, + new StaticWebApplicationContext().tap { + it.servletContext = servletContext + refresh() + beanFactory.registerSingleton(GrailsApplication.APPLICATION_ID, new DefaultGrailsApplication()) + } + ) + servletContext } private Supplier grailsApplicationSupplier() { From da7c42d523d6e002c147b354174f58c00517341c Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 10 Aug 2026 16:06:43 +0200 Subject: [PATCH 19/22] test: add multipart file size limit tests to FileUploadSpec --- .../fileupload/FileUploadSpec.groovy | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy index dbe2921e9d6..6c18d2923aa 100644 --- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy +++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy @@ -24,6 +24,7 @@ import spock.lang.Tag import grails.testing.mixin.integration.Integration import org.apache.grails.testing.http.client.HttpClientSupport import org.apache.grails.testing.http.client.MultipartBody +import org.springframework.test.context.TestPropertySource /** * Integration tests for file upload functionality in Grails. @@ -33,6 +34,10 @@ import org.apache.grails.testing.http.client.MultipartBody */ @Integration @Tag('http-client') +@TestPropertySource(properties = [ + 'spring.servlet.multipart.maxFileSize=2KB', + 'spring.servlet.multipart.maxRequestSize=3KB' +]) class FileUploadSpec extends Specification implements HttpClientSupport { // ========== Single File Upload Tests ========== @@ -285,6 +290,37 @@ class FileUploadSpec extends Specification implements HttpClientSupport { ]) } + def "upload over the configured multipart max file size is rejected"() { + given: + def content = 'X' * (2 * 1024 + 1) + def body = MultipartBody.builder() + .addPart('file', 'over-limit.txt', 'text/plain', content.bytes) + .build() + + when: + def response = httpPostMultipart('/fileUploadTest/uploadSingle', body) + + then: + response.assertStatus(413) + } + + def "upload within the configured multipart max file size succeeds"() { + given: + def content = 'X' * 1536 + def body = MultipartBody.builder() + .addPart('file', 'raised-limit.txt', 'text/plain', content.bytes) + .build() + + when: + def response = httpPostMultipart('/fileUploadTest/uploadSingle', body) + + then: + response.assertJsonContains(200, [ + success: true, + size : content.bytes.length + ]) + } + def "upload json file with content"() { given: def jsonContent = '{"users":[{"name":"Alice","age":30},{"name":"Bob","age":25}]}' From ca1ea870b77cdf14e4a443cc6d7fc4775640a1cb Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 10 Aug 2026 16:16:33 +0200 Subject: [PATCH 20/22] feat: implement environment post processor to validate legacy multipart configuration --- .../ControllersAutoConfiguration.java | 21 +------ ...lsControllersEnvironmentPostProcessor.java | 54 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 4 +- .../ControllersAutoConfigurationSpec.groovy | 29 ---------- ...rollersEnvironmentPostProcessorSpec.groovy | 55 +++++++++++++++++++ 5 files changed, 113 insertions(+), 50 deletions(-) create mode 100644 grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsControllersEnvironmentPostProcessor.java create mode 100644 grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/GrailsControllersEnvironmentPostProcessorSpec.groovy diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java index 9d261f3f3af..47a178a3287 100644 --- a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/ControllersAutoConfiguration.java @@ -31,8 +31,6 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.context.properties.bind.Bindable; -import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.servlet.autoconfigure.HttpEncodingAutoConfiguration; import org.springframework.boot.servlet.filter.OrderedCharacterEncodingFilter; import org.springframework.boot.web.servlet.FilterRegistrationBean; @@ -40,9 +38,7 @@ import org.springframework.boot.webmvc.autoconfigure.DispatcherServletRegistrationBean; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import org.springframework.context.ApplicationContext; -import org.springframework.context.EnvironmentAware; import org.springframework.context.annotation.Bean; -import org.springframework.core.env.Environment; import org.springframework.util.ClassUtils; import org.springframework.web.filter.CharacterEncodingFilter; import org.springframework.web.servlet.DispatcherServlet; @@ -63,15 +59,7 @@ after = {DomainClassAutoConfiguration.class} ) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) -public class ControllersAutoConfiguration implements EnvironmentAware { - - private static final String LEGACY_MULTIPART_CONFIGURATION = "grails.controllers.upload"; - - private static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = - "Configuration properties under 'grails.controllers.upload' are no longer supported. " + - "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + - "'spring.servlet.multipart.maxFileSize=200MB' and " + - "'spring.servlet.multipart.maxRequestSize=200MB'."; +public class ControllersAutoConfiguration { @Value("${" + Settings.FILTER_ENCODING + ":utf-8}") private String filtersEncoding; @@ -151,13 +139,6 @@ public FilterRegistrationBean grailsWebRequestFilter(Gra return registrationBean; } - @Override - public void setEnvironment(Environment environment) { - if (Binder.get(environment).bind(LEGACY_MULTIPART_CONFIGURATION, Bindable.mapOf(String.class, Object.class)).isBound()) { - throw new IllegalStateException(LEGACY_MULTIPART_CONFIGURATION_ERROR); - } - } - @Bean public DispatcherServlet dispatcherServlet() { return new GrailsDispatcherServlet(); diff --git a/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsControllersEnvironmentPostProcessor.java b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsControllersEnvironmentPostProcessor.java new file mode 100644 index 00000000000..3c075c4a7eb --- /dev/null +++ b/grails-controllers/src/main/groovy/org/grails/plugins/web/controllers/GrailsControllersEnvironmentPostProcessor.java @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.web.controllers; + +import org.jspecify.annotations.NonNull; + +import org.springframework.boot.EnvironmentPostProcessor; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.properties.bind.Bindable; +import org.springframework.boot.context.properties.bind.Binder; +import org.springframework.core.env.ConfigurableEnvironment; + +/** + * Environment post processor that checks for legacy multipart configuration properties. + * + * @since 8.0 + */ +public class GrailsControllersEnvironmentPostProcessor implements EnvironmentPostProcessor { + + private static final String LEGACY_MULTIPART_CONFIGURATION = "grails.controllers.upload"; + + private static final String LEGACY_MULTIPART_CONFIGURATION_ERROR = + "Configuration properties under 'grails.controllers.upload' are no longer supported. " + + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + + "'spring.servlet.multipart.maxFileSize=200MB' and " + + "'spring.servlet.multipart.maxRequestSize=200MB'."; + + @Override + public void postProcessEnvironment(@NonNull ConfigurableEnvironment environment, @NonNull SpringApplication application) { + var legacyMultipartConfig = Binder.get(environment) + .bind(LEGACY_MULTIPART_CONFIGURATION, Bindable.mapOf(String.class, Object.class)); + if (legacyMultipartConfig.isBound()) { + throw new IllegalStateException( + LEGACY_MULTIPART_CONFIGURATION_ERROR + + " Found: " + legacyMultipartConfig.get().keySet()); + } + } +} diff --git a/grails-controllers/src/main/resources/META-INF/spring.factories b/grails-controllers/src/main/resources/META-INF/spring.factories index 3a85f6e7d1c..be612f03128 100644 --- a/grails-controllers/src/main/resources/META-INF/spring.factories +++ b/grails-controllers/src/main/resources/META-INF/spring.factories @@ -16,4 +16,6 @@ # specific language governing permissions and limitations # under the License. # -org.springframework.boot.EnvironmentPostProcessor=org.grails.plugins.web.controllers.GrailsWebResourcesEnvironmentPostProcessor +org.springframework.boot.EnvironmentPostProcessor=\ + org.grails.plugins.web.controllers.GrailsWebResourcesEnvironmentPostProcessor,\ + org.grails.plugins.web.controllers.GrailsControllersEnvironmentPostProcessor diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy index f40f40f41b4..3de67d42c34 100644 --- a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/ControllersAutoConfigurationSpec.groovy @@ -24,7 +24,6 @@ import java.util.function.Supplier import grails.core.DefaultGrailsApplication import grails.core.GrailsApplication -import org.springframework.beans.factory.BeanCreationException import org.springframework.boot.autoconfigure.AutoConfigurations import org.springframework.boot.servlet.autoconfigure.MultipartAutoConfiguration import org.springframework.boot.test.context.runner.WebApplicationContextRunner @@ -35,12 +34,10 @@ import org.springframework.boot.webmvc.autoconfigure.DispatcherServletRegistrati import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration import org.springframework.context.ApplicationContext import org.springframework.context.ConfigurableApplicationContext -import org.springframework.core.env.MapPropertySource import org.springframework.mock.web.MockHttpServletRequest import org.springframework.mock.web.MockHttpServletResponse import org.springframework.mock.web.MockServletContext import org.springframework.web.context.WebApplicationContext -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext import org.springframework.web.context.support.StaticWebApplicationContext import org.springframework.web.filter.RequestContextFilter import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver @@ -59,32 +56,6 @@ class ControllersAutoConfigurationSpec extends Specification { def autoConfiguration = new ControllersAutoConfiguration() - def "legacy multipart configuration fails startup with migration instructions"() { - given: - def applicationContext = new AnnotationConfigWebApplicationContext().tap { - servletContext = new MockServletContext() - environment.propertySources.addFirst(new MapPropertySource('test', [ - 'grails.controllers.upload.maxFileSize': 20000000, - ])) - register(ControllersAutoConfiguration) - } - - when: - applicationContext.refresh() - - then: - def exception = thrown(BeanCreationException) - exception.rootCause instanceof IllegalStateException - exception.rootCause.message == - "Configuration properties under 'grails.controllers.upload' are no longer supported. " + - "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + - "'spring.servlet.multipart.maxFileSize=200MB' and " + - "'spring.servlet.multipart.maxRequestSize=200MB'." - - cleanup: - applicationContext.close() - } - void 'grailsWebRequest filter is a RequestContextFilter so Boot WebMvcAutoConfiguration backs off its own RequestContextFilter'() { when: 'the Grails request-binding filter bean is created' def filter = autoConfiguration.grailsWebRequest(applicationContext) diff --git a/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/GrailsControllersEnvironmentPostProcessorSpec.groovy b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/GrailsControllersEnvironmentPostProcessorSpec.groovy new file mode 100644 index 00000000000..b408b8accdb --- /dev/null +++ b/grails-controllers/src/test/groovy/org/grails/plugins/web/controllers/GrailsControllersEnvironmentPostProcessorSpec.groovy @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.grails.plugins.web.controllers + +import org.springframework.core.env.MapPropertySource +import org.springframework.core.env.StandardEnvironment + +import spock.lang.Specification + +class GrailsControllersEnvironmentPostProcessorSpec extends Specification { + + private final GrailsControllersEnvironmentPostProcessor processor = + new GrailsControllersEnvironmentPostProcessor() + + void 'legacy multipart configuration fails before context creation and identifies configured keys'() { + given: + def environment = new StandardEnvironment() + environment.propertySources.addFirst(new MapPropertySource('test', [ + 'grails.controllers.upload.maxFileSize': 20000000, + 'grails.controllers.upload.unused': true + ])) + + when: + processor.postProcessEnvironment(environment, null) + + then: + def exception = thrown(IllegalStateException) + exception.message == + "Configuration properties under 'grails.controllers.upload' are no longer supported. " + + "Use Spring Boot's 'spring.servlet.multipart' configuration instead. For example, set " + + "'spring.servlet.multipart.maxFileSize=200MB' and " + + "'spring.servlet.multipart.maxRequestSize=200MB'. Found: [maxFileSize, unused]" + } + + void 'an application without legacy multipart configuration passes the guard'() { + expect: + processor.postProcessEnvironment(new StandardEnvironment(), null) + } +} From 82d3011d3f4e3b180b4da87577b6b43ea49c6d4c Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Mon, 10 Aug 2026 16:36:43 +0200 Subject: [PATCH 21/22] docs: update multipart configuration documentation --- .../en/guide/theWebLayer/controllers/uploadingFiles.adoc | 2 +- grails-doc/src/en/guide/upgrading/upgrading80x.adoc | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc index efc930f88dd..fe472efe908 100644 --- a/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc +++ b/grails-doc/src/en/guide/theWebLayer/controllers/uploadingFiles.adoc @@ -114,7 +114,7 @@ spring: `maxRequestSize` = The maximum size allowed for a multipart/form-data request. -The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `fileSizeThreshold`, and `enabled` properties. +The `spring.servlet.multipart` namespace also supports Spring Boot's `location`, `fileSizeThreshold`, `enabled`, `resolveLazily`, and `strictServletCompliance` properties. See the {springBootReference}../appendix/application-properties/index.html[Spring Boot multipart application properties] reference for the complete list. The former `grails.controllers.upload` configuration is no longer supported. Applications using it fail at startup with a message explaining how to migrate to `spring.servlet.multipart`. You should keep in mind https://www.owasp.org/index.php/Unrestricted_File_Upload[OWASP recommendations - Unrestricted File Upload] diff --git a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc index 1649ff2b4a9..08c160d31ff 100644 --- a/grails-doc/src/en/guide/upgrading/upgrading80x.adoc +++ b/grails-doc/src/en/guide/upgrading/upgrading80x.adoc @@ -140,7 +140,7 @@ spring: ===== 4.2 Multipart Upload Configuration -The `grails.controllers.upload.*` configuration properties are no longer supported. +The `grails.controllers.upload.\*` configuration properties are no longer supported. Applications that still define any property under `grails.controllers.upload` fail at startup with migration instructions. Move those settings to Spring Boot's `spring.servlet.multipart.*` namespace: @@ -164,7 +164,10 @@ spring: maxRequestSize: 200MB ---- -Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.fileSizeThreshold`, and `spring.servlet.multipart.enabled` as replacements for the corresponding legacy settings. +Spring Boot also provides `spring.servlet.multipart.location`, `spring.servlet.multipart.fileSizeThreshold`, `spring.servlet.multipart.enabled`, `spring.servlet.multipart.resolveLazily`, and `spring.servlet.multipart.strictServletCompliance` as replacements for the corresponding legacy settings. See the {springBootReference}../appendix/application-properties/index.html[Spring Boot multipart application properties] reference for the complete list. + +Unlike the Grails 7 default, the multipart location is not set to `System.getProperty("java.io.tmpdir")`; when `spring.servlet.multipart.location` is not configured, the resulting `MultipartConfigElement` has an empty location and uploads use the servlet container's temporary directory. This can matter when the container's temporary storage is constrained or when an application expects uploaded files in `java.io.tmpdir`. +When no upload limits are configured, Spring Boot's defaults apply: 1MB per file and 10MB per request. Set `spring.servlet.multipart.maxFileSize` and `spring.servlet.multipart.maxRequestSize` explicitly if your application requires different limits. ==== 5. Hibernate ORM Package Relocations From 90dacd3edb9e29b2028746cc2be36b67f75d6cd6 Mon Sep 17 00:00:00 2001 From: Mattias Reichel Date: Tue, 11 Aug 2026 08:17:55 +0200 Subject: [PATCH 22/22] test: configure multipart file upload limits in example-app1 application.yml --- .../app1/grails-app/conf/application.yml | 6 +++++- .../functionaltests/fileupload/FileUploadSpec.groovy | 9 ++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/grails-test-examples/app1/grails-app/conf/application.yml b/grails-test-examples/app1/grails-app/conf/application.yml index 6a0676aba3f..ab5183f081c 100644 --- a/grails-test-examples/app1/grails-app/conf/application.yml +++ b/grails-test-examples/app1/grails-app/conf/application.yml @@ -64,7 +64,11 @@ grails: enabled: true mappings: '[/api/**]': default - +spring: + servlet: + multipart: + maxFileSize: 2KB + maxRequestSize: 3KB --- dataSource: pooled: true diff --git a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy index 6c18d2923aa..8ce89746aca 100644 --- a/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy +++ b/grails-test-examples/app1/src/integration-test/groovy/functionaltests/fileupload/FileUploadSpec.groovy @@ -24,7 +24,6 @@ import spock.lang.Tag import grails.testing.mixin.integration.Integration import org.apache.grails.testing.http.client.HttpClientSupport import org.apache.grails.testing.http.client.MultipartBody -import org.springframework.test.context.TestPropertySource /** * Integration tests for file upload functionality in Grails. @@ -34,10 +33,6 @@ import org.springframework.test.context.TestPropertySource */ @Integration @Tag('http-client') -@TestPropertySource(properties = [ - 'spring.servlet.multipart.maxFileSize=2KB', - 'spring.servlet.multipart.maxRequestSize=3KB' -]) class FileUploadSpec extends Specification implements HttpClientSupport { // ========== Single File Upload Tests ========== @@ -294,7 +289,7 @@ class FileUploadSpec extends Specification implements HttpClientSupport { given: def content = 'X' * (2 * 1024 + 1) def body = MultipartBody.builder() - .addPart('file', 'over-limit.txt', 'text/plain', content.bytes) + .addPart('file', 'over-size-limit.txt', 'text/plain', content.bytes) .build() when: @@ -308,7 +303,7 @@ class FileUploadSpec extends Specification implements HttpClientSupport { given: def content = 'X' * 1536 def body = MultipartBody.builder() - .addPart('file', 'raised-limit.txt', 'text/plain', content.bytes) + .addPart('file', 'within-size-limit.txt', 'text/plain', content.bytes) .build() when: