Skip to content

Commit a9f52c4

Browse files
committed
fix: refactor multipart file config
1 parent faf0267 commit a9f52c4

File tree

4 files changed

+304
-65
lines changed

4 files changed

+304
-65
lines changed
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package com.example.arInfra.config;
2+
3+
import com.example.arInfra.InfraGenerated;
4+
import com.example.arInfra.validator.file.MultipartPropertiesValidator;
5+
import jakarta.annotation.PostConstruct;
6+
import lombok.RequiredArgsConstructor;
7+
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
8+
import org.springframework.context.annotation.Configuration;
9+
10+
/**
11+
* Initializes and validates multipart file upload configuration at application startup.
12+
*
13+
* <p>This configuration class ensures that multipart upload limits are validated before the
14+
* application accepts any requests, providing fail-fast behavior for security misconfigurations.
15+
*/
16+
@Configuration
17+
@InfraGenerated
18+
@RequiredArgsConstructor
19+
public class MultipartConfigurationInitializer {
20+
21+
private final MultipartProperties multipartProperties;
22+
private final MultipartPropertiesValidator multipartPropertiesValidator;
23+
24+
/**
25+
* Validates multipart configuration at application startup.
26+
*
27+
* <p>If validation fails, the application will not start, ensuring that insecure configurations
28+
* are caught immediately rather than at runtime.
29+
*
30+
* @throws SecurityException if multipart configuration is insecure or missing
31+
*/
32+
@PostConstruct
33+
public void validateMultipartConfiguration() {
34+
multipartPropertiesValidator.validate(multipartProperties);
35+
}
36+
}

src/main/java/com/example/arInfra/config/MultipartConfigurer.java

Lines changed: 0 additions & 65 deletions
This file was deleted.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
package com.example.arInfra.validator.file;
2+
3+
import static java.lang.String.format;
4+
import static org.springframework.util.unit.DataSize.ofBytes;
5+
import static org.springframework.util.unit.DataSize.ofMegabytes;
6+
7+
import com.example.arInfra.InfraGenerated;
8+
import com.example.arInfra.validator.Validator;
9+
import jakarta.validation.constraints.NotNull;
10+
import lombok.extern.slf4j.Slf4j;
11+
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.util.unit.DataSize;
14+
import org.springframework.validation.annotation.Validated;
15+
16+
/**
17+
* Validator for multipart file upload configuration properties.
18+
*
19+
* <p>This validator ensures that file upload limits are explicitly configured and within acceptable
20+
* security bounds, preventing DoS attacks through unrestricted file uploads.
21+
*
22+
* <p>Security compliance:
23+
*
24+
* <ul>
25+
* <li>OWASP - Top 10 2021 Category A5 - Security Misconfiguration
26+
* <li>CWE-770 - Allocation of Resources Without Limits or Throttling
27+
* <li>CWE-400 - Uncontrolled Resource Consumption
28+
* </ul>
29+
*
30+
* <p>Validation rules:
31+
*
32+
* <ul>
33+
* <li>Max file size must be explicitly configured (not default/unlimited)
34+
* <li>Max request size must be explicitly configured
35+
* <li>Values must not exceed absolute maximum (100MB)
36+
* <li>Warns if values exceed OWASP recommendation (8MB)
37+
* </ul>
38+
*/
39+
@Slf4j
40+
@Component
41+
@Validated
42+
@InfraGenerated
43+
public class MultipartPropertiesValidator implements Validator<MultipartProperties> {
44+
45+
private static final DataSize RECOMMENDED_MAX_SIZE = ofMegabytes(8);
46+
private static final DataSize ABSOLUTE_MAX_SIZE = ofMegabytes(100);
47+
private static final DataSize ZERO = ofBytes(0);
48+
49+
@Override
50+
public void validate(@NotNull MultipartProperties properties) {
51+
validateMaxFileSize(properties.getMaxFileSize());
52+
validateMaxRequestSize(properties.getMaxRequestSize());
53+
54+
log.info(
55+
"Multipart configuration validated - maxFileSize: {}, maxRequestSize: {}",
56+
properties.getMaxFileSize(),
57+
properties.getMaxRequestSize());
58+
}
59+
60+
@Override
61+
public Class<MultipartProperties> getValidatedType() {
62+
return MultipartProperties.class;
63+
}
64+
65+
private void validateMaxFileSize(DataSize maxFileSize) {
66+
if (maxFileSize == null || maxFileSize.compareTo(ZERO) <= 0)
67+
throw new SecurityException(
68+
format(
69+
"%s %s",
70+
"Multipart max-file-size must be explicitly configured for security.",
71+
"Set spring.servlet.multipart.max-file-size in application.yml"));
72+
73+
if (maxFileSize.compareTo(ABSOLUTE_MAX_SIZE) > 0)
74+
throw new SecurityException(
75+
format(
76+
"Multipart max-file-size (%s) exceeds absolute maximum (%s). "
77+
+ "This poses a serious DoS risk.",
78+
maxFileSize, ABSOLUTE_MAX_SIZE));
79+
80+
if (maxFileSize.compareTo(RECOMMENDED_MAX_SIZE) > 0)
81+
log.warn(
82+
"Multipart max-file-size ({}) exceeds OWASP recommendation of {}. "
83+
+ "Ensure this is required for your use case.",
84+
maxFileSize,
85+
RECOMMENDED_MAX_SIZE);
86+
}
87+
88+
private void validateMaxRequestSize(DataSize maxRequestSize) {
89+
if (maxRequestSize == null || maxRequestSize.compareTo(ZERO) <= 0)
90+
throw new SecurityException(
91+
format(
92+
"%s %s",
93+
"Multipart max-request-size must be explicitly configured for security.",
94+
"Set spring.servlet.multipart.max-request-size in application.yml"));
95+
96+
if (maxRequestSize.compareTo(ABSOLUTE_MAX_SIZE) > 0)
97+
throw new SecurityException(
98+
format(
99+
"Multipart max-request-size (%s) exceeds absolute maximum (%s). "
100+
+ "This poses a serious DoS risk.",
101+
maxRequestSize, ABSOLUTE_MAX_SIZE));
102+
103+
if (maxRequestSize.compareTo(RECOMMENDED_MAX_SIZE) > 0)
104+
log.warn(
105+
"Multipart max-request-size ({}) exceeds OWASP recommendation of {}. "
106+
+ "Ensure this is required for your use case.",
107+
maxRequestSize,
108+
RECOMMENDED_MAX_SIZE);
109+
}
110+
}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
package com.example.arInfra.validator.file;
2+
3+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
4+
import static org.junit.jupiter.api.Assertions.assertEquals;
5+
import static org.junit.jupiter.api.Assertions.assertThrows;
6+
import static org.junit.jupiter.api.Assertions.assertTrue;
7+
import static org.springframework.util.unit.DataSize.ofBytes;
8+
import static org.springframework.util.unit.DataSize.ofMegabytes;
9+
10+
import com.example.arInfra.InfraGenerated;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
import org.springframework.boot.autoconfigure.web.servlet.MultipartProperties;
14+
15+
@InfraGenerated
16+
class MultipartPropertiesValidatorTest {
17+
18+
private MultipartPropertiesValidator subject;
19+
20+
@BeforeEach
21+
void setUp() {
22+
subject = new MultipartPropertiesValidator();
23+
}
24+
25+
@Test
26+
void validate_with_valid_configuration_should_succeed() {
27+
MultipartProperties properties = new MultipartProperties();
28+
properties.setMaxFileSize(ofMegabytes(8));
29+
properties.setMaxRequestSize(ofMegabytes(8));
30+
31+
assertDoesNotThrow(() -> subject.validate(properties));
32+
}
33+
34+
@Test
35+
void validate_with_5mb_limit_should_succeed() {
36+
MultipartProperties properties = new MultipartProperties();
37+
properties.setMaxFileSize(ofMegabytes(5));
38+
properties.setMaxRequestSize(ofMegabytes(5));
39+
40+
assertDoesNotThrow(() -> subject.validate(properties));
41+
}
42+
43+
@Test
44+
void validate_with_10mb_limit_should_succeed_with_warning() {
45+
MultipartProperties properties = new MultipartProperties();
46+
properties.setMaxFileSize(ofMegabytes(10));
47+
properties.setMaxRequestSize(ofMegabytes(10));
48+
49+
assertDoesNotThrow(() -> subject.validate(properties));
50+
}
51+
52+
@Test
53+
void validate_with_null_max_file_size_should_throw_exception() {
54+
MultipartProperties properties = new MultipartProperties();
55+
properties.setMaxFileSize(null);
56+
properties.setMaxRequestSize(ofMegabytes(8));
57+
58+
SecurityException exception =
59+
assertThrows(SecurityException.class, () -> subject.validate(properties));
60+
61+
assertTrue(exception.getMessage().contains("max-file-size"));
62+
assertTrue(exception.getMessage().contains("explicitly configured"));
63+
}
64+
65+
@Test
66+
void validate_with_zero_max_file_size_should_throw_exception() {
67+
MultipartProperties properties = new MultipartProperties();
68+
properties.setMaxFileSize(ofBytes(0));
69+
properties.setMaxRequestSize(ofMegabytes(8));
70+
71+
SecurityException exception =
72+
assertThrows(SecurityException.class, () -> subject.validate(properties));
73+
74+
assertTrue(exception.getMessage().contains("max-file-size"));
75+
}
76+
77+
@Test
78+
void validate_with_negative_max_file_size_should_throw_exception() {
79+
MultipartProperties properties = new MultipartProperties();
80+
properties.setMaxFileSize(ofBytes(-1));
81+
properties.setMaxRequestSize(ofMegabytes(8));
82+
83+
SecurityException exception =
84+
assertThrows(SecurityException.class, () -> subject.validate(properties));
85+
86+
assertTrue(exception.getMessage().contains("max-file-size"));
87+
}
88+
89+
@Test
90+
void validate_with_null_max_request_size_should_throw_exception() {
91+
MultipartProperties properties = new MultipartProperties();
92+
properties.setMaxFileSize(ofMegabytes(8));
93+
properties.setMaxRequestSize(null);
94+
95+
SecurityException exception =
96+
assertThrows(SecurityException.class, () -> subject.validate(properties));
97+
98+
assertTrue(exception.getMessage().contains("max-request-size"));
99+
assertTrue(exception.getMessage().contains("explicitly configured"));
100+
}
101+
102+
@Test
103+
void validate_with_excessive_max_file_size_should_throw_exception() {
104+
MultipartProperties properties = new MultipartProperties();
105+
properties.setMaxFileSize(ofMegabytes(150));
106+
properties.setMaxRequestSize(ofMegabytes(8));
107+
108+
SecurityException exception =
109+
assertThrows(SecurityException.class, () -> subject.validate(properties));
110+
111+
assertTrue(exception.getMessage().contains("exceeds absolute maximum"));
112+
assertTrue(exception.getMessage().contains("DoS risk"));
113+
}
114+
115+
@Test
116+
void validate_with_excessive_max_request_size_should_throw_exception() {
117+
MultipartProperties properties = new MultipartProperties();
118+
properties.setMaxFileSize(ofMegabytes(8));
119+
properties.setMaxRequestSize(ofMegabytes(150));
120+
121+
SecurityException exception =
122+
assertThrows(SecurityException.class, () -> subject.validate(properties));
123+
124+
assertTrue(exception.getMessage().contains("exceeds absolute maximum"));
125+
assertTrue(exception.getMessage().contains("DoS risk"));
126+
}
127+
128+
@Test
129+
void validate_with_exactly_100mb_limit_should_succeed() {
130+
MultipartProperties properties = new MultipartProperties();
131+
properties.setMaxFileSize(ofMegabytes(100));
132+
properties.setMaxRequestSize(ofMegabytes(100));
133+
134+
assertDoesNotThrow(() -> subject.validate(properties));
135+
}
136+
137+
@Test
138+
void validate_with_101mb_limit_should_throw_exception() {
139+
MultipartProperties properties = new MultipartProperties();
140+
properties.setMaxFileSize(ofMegabytes(101));
141+
properties.setMaxRequestSize(ofMegabytes(8));
142+
143+
SecurityException exception =
144+
assertThrows(SecurityException.class, () -> subject.validate(properties));
145+
146+
assertTrue(exception.getMessage().contains("exceeds absolute maximum"));
147+
}
148+
149+
@Test
150+
void get_validated_type_should_return_multipart_properties_class() {
151+
assertEquals(MultipartProperties.class, subject.getValidatedType());
152+
}
153+
154+
@Test
155+
void validate_with_null_properties_should_throw_exception() {
156+
assertThrows(NullPointerException.class, () -> subject.validate(null));
157+
}
158+
}

0 commit comments

Comments
 (0)