From ca3c1979b8fbd7faa88ee696356c37a07a234007 Mon Sep 17 00:00:00 2001 From: Rob Winch Date: Thu, 2 Aug 2012 16:43:24 -0500 Subject: [PATCH] SEC-2005: Ensure SecurityContext saved prior to the response being committed Previously Spring Security did not save the Security Context immediately prior to the following methods being invoked: - HttpServletResonse.flushBuffer() - HttpServletResonse.getWriter().close() - HttpServletResonse.getWriter().flush() - HttpServletRespose.getOutputStream().close() - HttpServletRespose.getOutputStream().flush() This meant that the client could get a response prior to the SecurityContext being stored. After the client got the response, it would make another request and this would not yet be authenticated. The reason this can occur is because all of the above methods commit the response, which means that the server can signal to the client the response is completed. A similar issue happened in SEC-398. Now the previously listed methods are wrapped in order to ensure the SecurityContext is persisted prior to the response being committed. --- ...ContextOnUpdateOrErrorResponseWrapper.java | 100 ++++++++++++++++- ...SessionSecurityContextRepositoryTests.java | 101 ++++++++++++++++++ 2 files changed, 198 insertions(+), 3 deletions(-) diff --git a/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java b/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java index c5c1670a70..7189136340 100644 --- a/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java +++ b/web/src/main/java/org/springframework/security/web/context/SaveContextOnUpdateOrErrorResponseWrapper.java @@ -1,7 +1,22 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed 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 + * + * http://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.springframework.security.web.context; import java.io.IOException; +import java.io.PrintWriter; +import java.io.Writer; +import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; @@ -10,8 +25,9 @@ import org.springframework.security.core.context.SecurityContextHolder; /** * Base class for response wrappers which encapsulate the logic for storing a security context and which - * store the with the SecurityContext when a sendError() or sendRedirect - * happens. See issue SEC-398. + * store the SecurityContext when a sendError(), sendRedirect, + * getOutputStream().close(), getOutputStream().flush(), getWriter().close(), or + * getWriter().flush() happens. See issue SEC-398 and SEC-2005. *

* Sub-classes should implement the {@link #saveContext(SecurityContext context)} method. *

@@ -19,6 +35,7 @@ import org.springframework.security.core.context.SecurityContextHolder; * * @author Luke Taylor * @author Marten Algesten + * @author Rob Winch * @since 3.0 */ public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServletResponseWrapper { @@ -74,6 +91,34 @@ public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServ super.sendRedirect(location); } + /** + * Makes sure the context is stored before calling getOutputStream().close() or + * getOutputStream().flush() + */ + @Override + public ServletOutputStream getOutputStream() throws IOException { + return new SaveContextServletOutputStream(super.getOutputStream()); + } + + /** + * Makes sure the context is stored before calling getWriter().close() or + * getWriter().flush() + */ + @Override + public PrintWriter getWriter() throws IOException { + return new SaveContextPrintWriter(super.getWriter()); + } + + /** + * Makes sure the context is stored before calling the + * superclass flushBuffer() + */ + @Override + public void flushBuffer() throws IOException { + doSaveContext(); + super.flushBuffer(); + } + /** * Calls saveContext() with the current contents of the SecurityContextHolder. */ @@ -115,10 +160,59 @@ public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServ } /** - * Tells if the response wrapper has called saveContext() because of an error or redirect. + * Tells if the response wrapper has called saveContext() because of this wrapper. */ public final boolean isContextSaved() { return contextSaved; } + /** + * Ensures the {@link SecurityContext} is updated prior to methods that commit the response. + * @author Rob Winch + */ + private class SaveContextPrintWriter extends PrintWriter { + + public SaveContextPrintWriter(Writer out) { + super(out); + } + + public void flush() { + doSaveContext(); + super.flush(); + } + + public void close() { + doSaveContext(); + super.close(); + } + } + + /** + * Ensures the {@link SecurityContext} is updated prior to methods that commit the response. + * + * @author Rob Winch + */ + private class SaveContextServletOutputStream extends ServletOutputStream { + private final ServletOutputStream delegate; + + public SaveContextServletOutputStream(ServletOutputStream delegate) { + this.delegate = delegate; + } + + public void write(int b) throws IOException { + this.delegate.write(b); + } + + @Override + public void flush() throws IOException { + doSaveContext(); + super.flush(); + } + + @Override + public void close() throws IOException { + doSaveContext(); + super.close(); + } + } } diff --git a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java index be66a06897..1817f890e3 100644 --- a/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java +++ b/web/src/test/java/org/springframework/security/web/context/HttpSessionSecurityContextRepositoryTests.java @@ -1,3 +1,15 @@ +/* + * Copyright 2002-2012 the original author or authors. + * + * Licensed 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 + * + * http://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.springframework.security.web.context; import static org.junit.Assert.*; @@ -17,6 +29,10 @@ import org.springframework.security.web.context.HttpRequestResponseHolder; import org.springframework.security.web.context.HttpSessionSecurityContextRepository; import org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper; +/** + * @author Luke Taylor + * @author Rob Winch + */ public class HttpSessionSecurityContextRepositoryTests { private final TestingAuthenticationToken testToken = new TestingAuthenticationToken("someone", "passwd", "ROLE_A"); @@ -141,6 +157,91 @@ public class HttpSessionSecurityContextRepositoryTests { assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); } + // SEC-2005 + @Test + public void flushBufferCausesEarlySaveOfContext() throws Exception { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContextHolder.setContext(repo.loadContext(holder)); + SecurityContextHolder.getContext().setAuthentication(testToken); + holder.getResponse().flushBuffer(); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved()); + repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse()); + // Check it's still the same + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + } + + // SEC-2005 + @Test + public void writerFlushCausesEarlySaveOfContext() throws Exception { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContextHolder.setContext(repo.loadContext(holder)); + SecurityContextHolder.getContext().setAuthentication(testToken); + holder.getResponse().getWriter().flush(); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved()); + repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse()); + // Check it's still the same + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + } + + // SEC-2005 + @Test + public void writerCloseCausesEarlySaveOfContext() throws Exception { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContextHolder.setContext(repo.loadContext(holder)); + SecurityContextHolder.getContext().setAuthentication(testToken); + holder.getResponse().getWriter().close(); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved()); + repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse()); + // Check it's still the same + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + } + + // SEC-2005 + @Test + public void outputStreamFlushCausesEarlySaveOfContext() throws Exception { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContextHolder.setContext(repo.loadContext(holder)); + SecurityContextHolder.getContext().setAuthentication(testToken); + holder.getResponse().getOutputStream().flush(); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved()); + repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse()); + // Check it's still the same + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + } + + // SEC-2005 + @Test + public void outputStreamCloseCausesEarlySaveOfContext() throws Exception { + HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response); + SecurityContextHolder.setContext(repo.loadContext(holder)); + SecurityContextHolder.getContext().setAuthentication(testToken); + holder.getResponse().getOutputStream().close(); + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + assertTrue(((SaveContextOnUpdateOrErrorResponseWrapper)holder.getResponse()).isContextSaved()); + repo.saveContext(SecurityContextHolder.getContext(), holder.getRequest(), holder.getResponse()); + // Check it's still the same + assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY)); + } + @Test public void noSessionIsCreatedIfSessionWasInvalidatedDuringTheRequest() throws Exception { HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();