Skip to content

Commit 82a53cd

Browse files
authored
Merge pull request #482 from TorMap/codex/propose-fix-for-etag-vulnerability
Restrict shallow ETag filter routes
2 parents 03ab4df + 2b43144 commit 82a53cd

3 files changed

Lines changed: 60 additions & 4 deletions

File tree

backend/src/main/kotlin/org/tormap/config/AppConfig.kt

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package org.tormap.config
22

33
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
4+
import org.springframework.boot.web.servlet.FilterRegistrationBean
45
import org.springframework.context.annotation.Bean
56
import org.springframework.context.annotation.Configuration
67
import org.springframework.scheduling.annotation.EnableAsync
@@ -30,12 +31,23 @@ class AppConfig : WebMvcConfigurer {
3031
}
3132

3233
/**
33-
* Include an unique ETag hash for each response to enable client side caching.
34+
* Include a weak ETag hash only for small, bounded GET endpoints.
35+
*
36+
* Shallow ETags require Spring to buffer the full response body before it can be hashed.
37+
* Keep this filter away from large public endpoints such as /relay/location/day/{day},
38+
* where buffering an entire day's relay locations can amplify memory and CPU usage.
3439
*/
3540
@Bean
36-
fun shallowEtagHeaderFilter(): ShallowEtagHeaderFilter {
41+
fun shallowEtagHeaderFilterRegistration(): FilterRegistrationBean<ShallowEtagHeaderFilter> {
3742
val filter = ShallowEtagHeaderFilter()
3843
filter.isWriteWeakETag = true
39-
return filter
44+
45+
return FilterRegistrationBean(filter).apply {
46+
addUrlPatterns(
47+
"/relay/location/days",
48+
"/relay/details/relay/*",
49+
"/relay/details/family/*",
50+
)
51+
}
4052
}
4153
}

backend/src/test/kotlin/org/tormap/config/SecurityConfigTest.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
package org.tormap.config
22

33
import io.kotest.core.spec.style.StringSpec
4+
import io.kotest.matchers.shouldBe
45
import io.kotest.matchers.shouldNotBe
56
import org.springframework.boot.test.context.SpringBootTest
7+
import org.springframework.boot.web.servlet.FilterRegistrationBean
68
import org.springframework.context.ApplicationContext
79
import org.springframework.security.core.userdetails.UserDetailsService
810
import org.springframework.security.crypto.password.PasswordEncoder
911
import org.springframework.security.web.SecurityFilterChain
1012
import org.springframework.test.context.ActiveProfiles
1113
import org.springframework.web.cors.CorsConfigurationSource
1214
import org.springframework.web.filter.ForwardedHeaderFilter
15+
import org.springframework.web.filter.ShallowEtagHeaderFilter
1316

1417
@SpringBootTest
1518
@ActiveProfiles("test")
@@ -19,7 +22,8 @@ class SecurityConfigTest(
1922
private val userDetailsService: UserDetailsService,
2023
private val corsConfigurationSource: CorsConfigurationSource,
2124
private val forwardedHeaderFilter: ForwardedHeaderFilter,
22-
private val securityFilterChain: SecurityFilterChain
25+
private val securityFilterChain: SecurityFilterChain,
26+
private val shallowEtagHeaderFilterRegistration: FilterRegistrationBean<ShallowEtagHeaderFilter>,
2327
) : StringSpec({
2428

2529
"security beans are loaded" {
@@ -31,4 +35,12 @@ class SecurityConfigTest(
3135
securityFilterChain shouldNotBe null
3236
}
3337

38+
"shallow ETag filter is not registered for large relay location responses" {
39+
shallowEtagHeaderFilterRegistration.urlPatterns shouldBe setOf(
40+
"/relay/location/days",
41+
"/relay/details/relay/*",
42+
"/relay/details/family/*",
43+
)
44+
}
45+
3446
})

backend/src/test/kotlin/org/tormap/controller/HttpCachingConfigTest.kt

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package org.tormap.controller
22

33
import io.kotest.core.spec.style.StringSpec
44
import org.hamcrest.Matchers.containsString
5+
import org.tormap.database.repository.RelayDetailsRepository
6+
import org.tormap.mockRelayDetails
57
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
68
import org.springframework.boot.test.context.SpringBootTest
79
import org.springframework.test.context.ActiveProfiles
@@ -15,7 +17,37 @@ import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
1517
@ActiveProfiles("test")
1618
class HttpCachingConfigTest(
1719
private val mockMvc: MockMvc,
20+
private val relayDetailsRepository: RelayDetailsRepository,
1821
) : StringSpec({
22+
val testRelay = relayDetailsRepository.save(
23+
mockRelayDetails().apply {
24+
familyId = 123L
25+
}
26+
)
27+
28+
"GET /relay/location/days includes ETag header" {
29+
mockMvc.perform(get("/relay/location/days"))
30+
.andExpect(status().isOk)
31+
.andExpect(header().exists("ETag"))
32+
}
33+
34+
"GET /relay/details/relay/{id} includes ETag header" {
35+
mockMvc.perform(get("/relay/details/relay/${testRelay.id}"))
36+
.andExpect(status().isOk)
37+
.andExpect(header().exists("ETag"))
38+
}
39+
40+
"GET /relay/details/family/{id} includes ETag header" {
41+
mockMvc.perform(get("/relay/details/family/${testRelay.familyId}"))
42+
.andExpect(status().isOk)
43+
.andExpect(header().exists("ETag"))
44+
}
45+
46+
"GET /relay/location/day/{day} does not include ETag header" {
47+
mockMvc.perform(get("/relay/location/day/2022-02-04"))
48+
.andExpect(status().isOk)
49+
.andExpect(header().doesNotExist("ETag"))
50+
}
1951

2052
"GET /relay/location/days includes Cache-Control public max-age" {
2153
mockMvc.perform(get("/relay/location/days"))

0 commit comments

Comments
 (0)