diff --git a/web/src/main/java/org/springframework/security/web/header/HeaderWriterFilter.java b/web/src/main/java/org/springframework/security/web/header/HeaderWriterFilter.java index c1a7b8fd4a4..77b6a625b99 100644 --- a/web/src/main/java/org/springframework/security/web/header/HeaderWriterFilter.java +++ b/web/src/main/java/org/springframework/security/web/header/HeaderWriterFilter.java @@ -18,6 +18,7 @@ import java.io.IOException; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import jakarta.servlet.FilterChain; import jakarta.servlet.RequestDispatcher; @@ -114,6 +115,8 @@ class HeaderWriterResponse extends OnCommittedResponseWrapper { private final HttpServletRequest request; + private final AtomicBoolean headersWritten = new AtomicBoolean(false); + HeaderWriterResponse(HttpServletRequest request, HttpServletResponse response) { super(response); this.request = request; @@ -129,7 +132,9 @@ protected void writeHeaders() { if (isDisableOnResponseCommitted()) { return; } - HeaderWriterFilter.this.writeHeaders(this.request, getHttpResponse()); + if (this.headersWritten.compareAndSet(false, true)) { + HeaderWriterFilter.this.writeHeaders(this.request, getHttpResponse()); + } } private HttpServletResponse getHttpResponse() { diff --git a/web/src/test/java/org/springframework/security/web/header/HeaderWriterFilterTests.java b/web/src/test/java/org/springframework/security/web/header/HeaderWriterFilterTests.java index 3ebcf096b1f..7476e4f1941 100644 --- a/web/src/test/java/org/springframework/security/web/header/HeaderWriterFilterTests.java +++ b/web/src/test/java/org/springframework/security/web/header/HeaderWriterFilterTests.java @@ -113,6 +113,26 @@ public void doFilterWhenRequestContainsIncludeThenHeadersStillWritten() throws E verifyNoMoreInteractions(this.writer1); } + // gh-9175 + @Test + public void doFilterWhenWriteHeadersCalledConcurrentlyThenHeadersWrittenOnlyOnce() throws Exception { + List headerWriters = new ArrayList<>(); + headerWriters.add(this.writer1); + HeaderWriterFilter filter = new HeaderWriterFilter(headerWriters); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + filter.doFilter(request, response, (req, resp) -> { + // Calling writeHeaders() directly simulates the race window where an + // async thread enters writeHeaders() via onResponseCommitted() but has + // not yet called disableOnResponseCommitted(). + ((HeaderWriterFilter.HeaderWriterResponse) resp).writeHeaders(); + }); + // The finally block in doHeadersAfter also calls writeHeaders(). + // Without the fix, the header writers would be invoked twice. + verify(this.writer1).writeHeaders(any(HttpServletRequest.class), any(HttpServletResponse.class)); + verifyNoMoreInteractions(this.writer1); + } + @Test public void headersWrittenAtBeginningOfRequest() throws Exception { HeaderWriterFilter filter = new HeaderWriterFilter(Collections.singletonList(this.writer1));