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.
This commit is contained in:
Rob Winch 2012-08-02 15:26:14 -05:00
parent ffe2834f4c
commit 1ab068a06d
2 changed files with 203 additions and 3 deletions

View File

@ -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 <code>SecurityContext</code> when a <code>sendError()</code> or <code>sendRedirect</code>
* happens. See issue SEC-398.
* store the <code>SecurityContext</code> when a <code>sendError()</code>, <code>sendRedirect</code>,
* <code>getOutputStream().close()</code>, <code>getOutputStream().flush()</code>, <code>getWriter().close()</code>, or
* <code>getWriter().flush()</code> happens. See issue SEC-398 and SEC-2005.
* <p>
* Sub-classes should implement the {@link #saveContext(SecurityContext context)} method.
* <p>
@ -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 <code>getOutputStream().close()</code> or
* <code>getOutputStream().flush()</code>
*/
@Override
public ServletOutputStream getOutputStream() throws IOException {
return new SaveContextServletOutputStream(super.getOutputStream());
}
/**
* Makes sure the context is stored before calling <code>getWriter().close()</code> or
* <code>getWriter().flush()</code>
*/
@Override
public PrintWriter getWriter() throws IOException {
return new SaveContextPrintWriter(super.getWriter());
}
/**
* Makes sure the context is stored before calling the
* superclass <code>flushBuffer()</code>
*/
@Override
public void flushBuffer() throws IOException {
doSaveContext();
super.flushBuffer();
}
/**
* Calls <code>saveContext()</code> with the current contents of the <tt>SecurityContextHolder</tt>.
*/
@ -115,10 +160,59 @@ public abstract class SaveContextOnUpdateOrErrorResponseWrapper extends HttpServ
}
/**
* Tells if the response wrapper has called <code>saveContext()</code> because of an error or redirect.
* Tells if the response wrapper has called <code>saveContext()</code> 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();
}
}
}

View File

@ -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.*;
@ -16,6 +28,10 @@ import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
/**
* @author Luke Taylor
* @author Rob Winch
*/
public class HttpSessionSecurityContextRepositoryTests {
private final TestingAuthenticationToken testToken = new TestingAuthenticationToken("someone", "passwd", "ROLE_A");
@ -151,6 +167,96 @@ public class HttpSessionSecurityContextRepositoryTests {
assertEquals(SecurityContextHolder.getContext(), request.getSession().getAttribute("imTheContext"));
}
// SEC-2005
@Test
public void flushBufferCausesEarlySaveOfContext() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
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("imTheContext"));
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("imTheContext"));
}
// SEC-2005
@Test
public void writerFlushCausesEarlySaveOfContext() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
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("imTheContext"));
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("imTheContext"));
}
// SEC-2005
@Test
public void writerCloseCausesEarlySaveOfContext() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
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("imTheContext"));
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("imTheContext"));
}
// SEC-2005
@Test
public void outputStreamFlushCausesEarlySaveOfContext() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
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("imTheContext"));
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("imTheContext"));
}
// SEC-2005
@Test
public void outputStreamCloseCausesEarlySaveOfContext() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();
repo.setSpringSecurityContextKey("imTheContext");
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("imTheContext"));
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("imTheContext"));
}
@Test
public void noSessionIsCreatedIfSessionWasInvalidatedDuringTheRequest() throws Exception {
HttpSessionSecurityContextRepository repo = new HttpSessionSecurityContextRepository();