Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bfdbbe8
PR for: https://github.com/apache/grails-core/issues/15644
gsartori Jul 20, 2026
d1ca11f
Merge fixes & cleanups
gsartori Jul 20, 2026
ce6c4e5
Merge branch 'apache:8.0.x' into 8.0.x
gsartori Jul 22, 2026
3a07ad2
Implemented @matrei feedback
gsartori Jul 22, 2026
b255caa
Upgrade guide
gsartori Jul 23, 2026
2f9e4e5
Cleanups: Update legacy maxFileSize
gsartori Jul 23, 2026
a360262
Reintroduced camelCase for multipart properties to keep consistency w…
gsartori Jul 27, 2026
d47aedc
Merge branch 'apache:8.0.x' into 8.0.x
gsartori Jul 27, 2026
8b63451
Implemented @matrei feedback
gsartori Jul 22, 2026
984e7c8
Upgrade guide
gsartori Jul 23, 2026
5e90de8
Cleanups: Update legacy maxFileSize
gsartori Jul 23, 2026
43c1b91
Reintroduced camelCase for multipart properties to keep consistency w…
gsartori Jul 27, 2026
0ff942e
Merge remote-tracking branch 'origin/use-spring-upload-config' into u…
gsartori Jul 28, 2026
8f9c9f8
Merge branch 'apache:8.0.x' into 8.0.x
gsartori Jul 28, 2026
925ffc8
Implemented @matrei feedback
gsartori Jul 22, 2026
c14c0d9
Upgrade guide
gsartori Jul 23, 2026
ab0c2be
Cleanups: Update legacy maxFileSize
gsartori Jul 23, 2026
1de6ba4
Reintroduced camelCase for multipart properties to keep consistency w…
gsartori Jul 27, 2026
ba07ad5
Merge remote-tracking branch 'origin/use-spring-upload-config' into u…
gsartori Jul 28, 2026
fc00254
Merge branch 'apache:8.0.x' into 8.0.x
gsartori Jul 29, 2026
5ecdafc
Merge branch '8.0.x' into use-spring-upload-config
jamesfredley Jul 29, 2026
cf2e542
Fix full stack trace output when System.err changes
gsartori Aug 5, 2026
9c8cd61
Merge branch 'apache:8.0.x' into 8.0.x
gsartori Aug 5, 2026
c96bcb3
Merge branch '8.0.x' into use-spring-upload-config
gsartori Aug 5, 2026
3941a0c
Revert "Fix full stack trace output when System.err changes"
matrei Aug 5, 2026
d62fd9c
test: add multipart configuration tests for dispatcher servlet regist…
matrei Aug 10, 2026
7488c87
test: simplify `ControllersAutoConfigurationSpec`
matrei Aug 10, 2026
da7c42d
test: add multipart file size limit tests to FileUploadSpec
matrei Aug 10, 2026
ca1ea87
feat: implement environment post processor to validate legacy multipa…
matrei Aug 10, 2026
82d3011
docs: update multipart configuration documentation
matrei Aug 10, 2026
09fee3d
Merge branch '8.0.x' into fork/gsartori/use-spring-upload-config
matrei Aug 10, 2026
90dacd3
test: configure multipart file upload limits in example-app1 applicat…
matrei Aug 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions THREAT_MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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? |
Expand All @@ -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)* |
Expand Down Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
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;
Expand Down Expand Up @@ -75,18 +76,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;

Expand Down Expand Up @@ -150,29 +139,21 @@ public FilterRegistrationBean<GrailsWebRequestFilter> grailsWebRequestFilter(Gra
return registrationBean;
}

@Bean
public MultipartConfigElement multipartConfigElement() {
if (uploadTmpDir == null) {
uploadTmpDir = System.getProperty("java.io.tmpdir");
}
return new MultipartConfigElement(uploadTmpDir, maxFileSize, maxRequestSize, fileSizeThreshold);
}

@Bean
public DispatcherServlet dispatcherServlet() {
return new GrailsDispatcherServlet();
}

@Bean
public DispatcherServletRegistrationBean dispatcherServletRegistration(GrailsApplication application, DispatcherServlet dispatcherServlet, MultipartConfigElement multipartConfigElement) {
public DispatcherServletRegistrationBean dispatcherServletRegistration(GrailsApplication application, DispatcherServlet dispatcherServlet, ObjectProvider<MultipartConfigElement> 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;
}
DispatcherServletRegistrationBean dispatcherServletRegistration = new DispatcherServletRegistrationBean(dispatcherServlet, grailsServletPath);
dispatcherServletRegistration.setLoadOnStartup(2);
dispatcherServletRegistration.setAsyncSupported(true);
dispatcherServletRegistration.setMultipartConfig(multipartConfigElement);
multipartConfigElement.ifAvailable(dispatcherServletRegistration::setMultipartConfig);
return dispatcherServletRegistration;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading