View Javadoc

1   package com.atlassian.plugins.rest.common.security.jersey;
2   
3   import com.atlassian.plugin.tracker.PluginModuleTracker;
4   import com.atlassian.plugins.rest.common.security.XsrfCheckFailedException;
5   import com.atlassian.plugins.rest.common.security.descriptor.CorsDefaults;
6   import com.atlassian.plugins.rest.common.security.descriptor.CorsDefaultsModuleDescriptor;
7   import com.atlassian.sal.api.web.context.HttpContext;
8   import com.atlassian.sal.core.xsrf.IndependentXsrfTokenAccessor;
9   import com.atlassian.sal.core.xsrf.IndependentXsrfTokenValidator;
10  import com.atlassian.sal.core.xsrf.XsrfRequestValidatorImpl;
11  import com.google.common.collect.ImmutableList;
12  import com.google.common.collect.ImmutableMap;
13  import com.sun.jersey.spi.container.ContainerRequest;
14  import org.junit.Before;
15  import org.junit.Test;
16  import org.junit.runner.RunWith;
17  import org.mockito.Mock;
18  import org.mockito.runners.MockitoJUnitRunner;
19  
20  import javax.servlet.http.Cookie;
21  import javax.servlet.http.HttpServletRequest;
22  import javax.ws.rs.core.MediaType;
23  import java.net.URI;
24  import java.util.Arrays;
25  import java.util.Collections;
26  
27  import static org.hamcrest.CoreMatchers.is;
28  import static org.junit.Assert.assertEquals;
29  import static org.junit.Assert.assertFalse;
30  import static org.junit.Assert.assertThat;
31  import static org.junit.Assert.assertTrue;
32  import static org.mockito.Mockito.mock;
33  import static org.mockito.Mockito.when;
34  
35  /**
36   */
37  @RunWith(MockitoJUnitRunner.class)
38  public class TestXsrfResourceFilter {
39      private static final String URI_ONE = "http://foo.com/hello";
40      private static final String URI_TWO = "http://bar.com/hello";
41      private static final String URI_ONE_HTTPS = "https://foo.com/hello";
42      private static final String URI_ONE_CUSTOM_PORT = "http://foo.com:1234/hello";
43      private static final String URI_ONE_NO_PATH = "http://foo.com";
44      private static final String URI_ONE_INVALID_QUERY_PARAM = URI_ONE + "?|";
45      private static final String INVALID_URI = "!!! oh noes, invalid URI :O";
46      private static final String URI_CHROME_EXTENSION = "chrome-extension://foobar";
47      private static final String URI_SAFARI_EXTENSION = "safari-extension://foobar";
48  
49      private XsrfResourceFilter xsrfResourceFilter;
50      @Mock
51      private ContainerRequest request;
52      @Mock
53      private HttpContext httpContext;
54      @Mock
55      private HttpServletRequest httpServletRequest;
56  
57      @Before
58      public void setUp() throws Exception {
59          xsrfResourceFilter = new XsrfResourceFilter();
60          xsrfResourceFilter.setXsrfRequestValidator(
61                  new XsrfRequestValidatorImpl(
62                          new IndependentXsrfTokenValidator(
63                                  new IndependentXsrfTokenAccessor())));
64          when(httpContext.getRequest()).thenReturn(httpServletRequest);
65          xsrfResourceFilter.setHttpContext(httpContext);
66  
67          when(request.getRequestUri()).thenReturn(new URI("http://default.com"));
68      }
69  
70      @Test(expected = XsrfCheckFailedException.class)
71      public void testGetProtectedByXsrfChecks() {
72          when(request.getMethod()).thenReturn("GET");
73          addTokenHeaderToRequest(httpServletRequest, "invalid");
74          assertEquals(request, xsrfResourceFilter.filter(request));
75      }
76  
77      @Test(expected = XsrfCheckFailedException.class)
78      public void testPostBlocked() {
79          when(request.getMethod()).thenReturn("POST");
80          when(request.getMediaType()).thenReturn(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
81          xsrfResourceFilter.filter(request);
82      }
83  
84      @Test
85      public void testPostSuccessWithCorrectHeaderValue() {
86          when(request.getMediaType()).thenReturn(
87                  MediaType.APPLICATION_FORM_URLENCODED_TYPE);
88          when(request.getMethod()).thenReturn("POST");
89          addTokenHeaderToRequest(httpServletRequest,
90                  XsrfResourceFilter.NO_CHECK);
91          assertEquals(request, xsrfResourceFilter.filter(request));
92      }
93  
94      @Test
95      public void testPostSuccessWithOldHeaderValue() {
96          final String deprecatedHeaderValue = "nocheck";
97          when(request.getMediaType()).thenReturn(
98                  MediaType.APPLICATION_FORM_URLENCODED_TYPE);
99          when(request.getMethod()).thenReturn("POST");
100         when(request.getHeaderValue(XsrfResourceFilter.TOKEN_HEADER)).
101                 thenReturn(deprecatedHeaderValue);
102         assertEquals(request, xsrfResourceFilter.filter(request));
103     }
104 
105     @Test(expected = XsrfCheckFailedException.class)
106     public void testPostFailureWithInvalidHeaderValue() {
107         when(request.getMediaType()).thenReturn(
108                 MediaType.APPLICATION_FORM_URLENCODED_TYPE);
109         when(request.getMethod()).thenReturn("POST");
110         addTokenHeaderToRequest(httpServletRequest, "invalid");
111         assertEquals(request, xsrfResourceFilter.filter(request));
112     }
113 
114     @Test
115     public void testPostJsonSuccess() {
116         when(request.getMethod()).thenReturn("POST");
117         when(request.getMediaType()).thenReturn(MediaType.APPLICATION_JSON_TYPE);
118         assertEquals(request, xsrfResourceFilter.filter(request));
119     }
120 
121     @Test
122     public void testPutFormSuccess() {
123         when(request.getMethod()).thenReturn("PUT");
124         when(request.getMediaType()).thenReturn(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
125         assertEquals(request, xsrfResourceFilter.filter(request));
126     }
127 
128     @Test
129     public void testPostWithValidXsrfTokenSuccess() {
130         when(request.getMethod()).thenReturn("POST");
131         when(request.getMediaType()).thenReturn(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
132         for (String validToken : Arrays.asList("abc123", "ffff", "FFFx3~324", "☃☃☃")) {
133             httpServletRequest = setupRequestWithXsrfToken(httpServletRequest, validToken, validToken);
134             assertThat(xsrfResourceFilter.filter(request), is(request));
135         }
136     }
137 
138     @Test(expected = XsrfCheckFailedException.class)
139     public void testPostWithInvalidXsrfTokenBlocked() {
140         when(request.getMethod()).thenReturn("POST");
141         when(request.getMediaType()).thenReturn(MediaType.APPLICATION_FORM_URLENCODED_TYPE);
142         httpServletRequest = setupRequestWithXsrfToken(httpServletRequest, "abc123", "notabc123");
143         xsrfResourceFilter.filter(request);
144     }
145 
146     @Test(expected = XsrfCheckFailedException.class)
147     public void testPostWithNullHttpServletRequestXsrfTokenBlocked() {
148         when(request.getMethod()).thenReturn("POST");
149         when(request.getMediaType()).thenReturn(
150                 MediaType.APPLICATION_FORM_URLENCODED_TYPE);
151         when(httpContext.getRequest()).thenReturn(null);
152         xsrfResourceFilter.setHttpContext(httpContext);
153         xsrfResourceFilter.filter(request);
154     }
155 
156     static void addTokenHeaderToRequest(
157             HttpServletRequest httpServletRequest, String headerValue) {
158         when(httpServletRequest.getHeader(XsrfResourceFilter.TOKEN_HEADER))
159                 .thenReturn(headerValue);
160     }
161 
162     private static HttpServletRequest setupRequestWithXsrfToken(
163             HttpServletRequest request, String expectedToken, String formToken) {
164         Cookie[] cookies = {new Cookie(IndependentXsrfTokenAccessor.XSRF_COOKIE_KEY, expectedToken)};
165         when(request.getCookies()).thenReturn(cookies);
166         when(request.getParameter(IndependentXsrfTokenValidator.XSRF_PARAM_NAME)).thenReturn(formToken);
167         return request;
168     }
169 
170     @Test
171     public void additionalBrowserChecksWithNullOriginAndReferrerFail() throws Exception {
172         assertFalse(runAdditionalBrowserChecks(URI_ONE, null, null));
173     }
174 
175     @Test
176     public void additionalBrowserChecksWithEmptyOriginAndReferrerFail() throws Exception {
177         assertFalse(runAdditionalBrowserChecks(URI_ONE, "", ""));
178     }
179 
180     @Test
181     public void additionalBrowserChecksWithInvalidOriginAndReferrerFail() throws Exception {
182         assertFalse(runAdditionalBrowserChecks(URI_ONE, INVALID_URI, INVALID_URI));
183     }
184 
185     @Test
186     public void additionalBrowserChecksWithSameOriginPass() throws Exception {
187         assertTrue(runAdditionalBrowserChecks(URI_ONE, URI_ONE, URI_TWO));
188     }
189 
190     @Test
191     public void additionalBrowserChecksWithSameOriginAndDifferentPathPass() throws Exception {
192         assertTrue(runAdditionalBrowserChecks(URI_ONE, URI_ONE_NO_PATH, URI_TWO));
193     }
194 
195     @Test
196     public void additionalBrowserChecksWithSameOriginButDifferentSecurityLevelFail() throws Exception {
197         assertFalse(runAdditionalBrowserChecks(URI_ONE_HTTPS, URI_ONE, URI_TWO));
198         assertFalse(runAdditionalBrowserChecks(URI_ONE, URI_ONE_HTTPS, URI_TWO));
199     }
200 
201     @Test
202     public void additionalBrowserChecksWithSameOriginButDifferentPortsFail() throws Exception {
203         assertFalse(runAdditionalBrowserChecks(URI_ONE, URI_ONE_CUSTOM_PORT, URI_TWO));
204         assertFalse(runAdditionalBrowserChecks(URI_ONE_CUSTOM_PORT, URI_ONE, URI_TWO));
205     }
206 
207     @Test
208     public void additionalBrowserChecksWithSameReferrerPass() throws Exception {
209         assertTrue(runAdditionalBrowserChecks(URI_ONE, URI_TWO, URI_ONE));
210     }
211 
212     @Test
213     public void additionalBrowserChecksWithSameOriginReferrerWithInvalidQueryParamPass() throws Exception {
214         assertTrue(runAdditionalBrowserChecks(URI_ONE, "", URI_ONE_INVALID_QUERY_PARAM));
215     }
216 
217     @Test
218     public void additionalBrowserChecksWithSameReferrerButDifferentSecurityLevelFail() throws Exception {
219         assertFalse(runAdditionalBrowserChecks(URI_ONE_HTTPS, URI_TWO, URI_ONE));
220         assertFalse(runAdditionalBrowserChecks(URI_ONE, URI_TWO, URI_ONE_HTTPS));
221     }
222 
223     @Test
224     public void additionalBrowserChecksWithSameReferrerButDifferentPortsFail() throws Exception {
225         assertFalse(runAdditionalBrowserChecks(URI_ONE, URI_TWO, URI_ONE_CUSTOM_PORT));
226         assertFalse(runAdditionalBrowserChecks(URI_ONE_CUSTOM_PORT, URI_TWO, URI_ONE));
227     }
228 
229     @Test
230     public void additionalBrowserChecksWithDifferentOriginAndReferrerFail() throws Exception {
231         assertFalse(runAdditionalBrowserChecks(URI_ONE, URI_TWO, URI_TWO));
232     }
233 
234     @Test
235     public void additionalBrowserChecksWithBrowserExtensionOriginPass() throws Exception {
236         assertThat(runAdditionalBrowserChecks(URI_ONE, URI_CHROME_EXTENSION, ""), is(true));
237         assertThat(runAdditionalBrowserChecks(URI_ONE_HTTPS, URI_SAFARI_EXTENSION, ""), is(true));
238     }
239 
240     private boolean runAdditionalBrowserChecks(final String uri, final String origin, final String referrer) throws Exception {
241         xsrfResourceFilter.setPluginModuleTracker(emptyPluginModuleTracker());
242 
243         when(request.getHeaderValue("Origin")).thenReturn(origin);
244         when(request.getHeaderValue("Referer")).thenReturn(referrer);
245         when(request.getRequestUri()).thenReturn(new URI(uri));
246 
247         return xsrfResourceFilter.passesAdditionalBrowserChecks(request);
248     }
249 
250     @Test
251     public void additionalBrowserChecksPassIfCorsCredentialsNotAllowedAndNotSupplied() {
252         xsrfResourceFilter.setPluginModuleTracker(pluginModuleTracker(URI_ONE, true, false));
253 
254         when(request.getHeaderValue("Origin")).thenReturn(URI_ONE);
255 
256         assertTrue(xsrfResourceFilter.passesAdditionalBrowserChecks(request));
257     }
258 
259     @Test
260     public void additionalBrowserChecksPassIfCorsCredentialsAllowedAndNotSupplied() {
261         xsrfResourceFilter.setPluginModuleTracker(pluginModuleTracker(URI_ONE, true, true));
262 
263         when(request.getHeaderValue("Origin")).thenReturn(URI_ONE);
264 
265         assertTrue(xsrfResourceFilter.passesAdditionalBrowserChecks(request));
266     }
267 
268     @Test
269     public void additionalBrowserChecksPassIfCorsCredentialsAllowedAndCookiesSupplied() {
270         xsrfResourceFilter.setPluginModuleTracker(pluginModuleTracker(URI_ONE, true, true));
271 
272         when(request.getHeaderValue("Origin")).thenReturn(URI_ONE);
273         setArbitraryCookie(request);
274 
275         assertTrue(xsrfResourceFilter.passesAdditionalBrowserChecks(request));
276     }
277 
278     @Test
279     public void additionalBrowserChecksPassIfCorsCredentialsAllowedAndHttpAuthHeaderSupplied() {
280         xsrfResourceFilter.setPluginModuleTracker(pluginModuleTracker(URI_ONE, true, true));
281 
282         when(request.getHeaderValue("Origin")).thenReturn(URI_ONE);
283         when(request.getHeaderValue("Authorization")).thenReturn("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
284 
285         assertTrue(xsrfResourceFilter.passesAdditionalBrowserChecks(request));
286     }
287 
288     @Test
289     public void additionalBrowserChecksFailIfCorsCredentialsDisallowedButCookiesSupplied() {
290         xsrfResourceFilter.setPluginModuleTracker(pluginModuleTracker(URI_ONE, true, false));
291 
292         when(request.getHeaderValue("Origin")).thenReturn(URI_ONE);
293         setArbitraryCookie(request);
294 
295         assertFalse(xsrfResourceFilter.passesAdditionalBrowserChecks(request));
296     }
297 
298     @Test
299     public void additionalBrowserChecksFailIfCorsCredentialsDisallowedButHttpAuthHeaderSupplied() {
300         xsrfResourceFilter.setPluginModuleTracker(pluginModuleTracker(URI_ONE, true, false));
301 
302         when(request.getHeaderValue("Origin")).thenReturn(URI_ONE);
303         when(request.getHeaderValue("Authorization")).thenReturn("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
304 
305         assertFalse(xsrfResourceFilter.passesAdditionalBrowserChecks(request));
306     }
307 
308     @Test
309     public void additionalBrowserChecksTakePortNumbersIntoAccountWhenCheckingAllowedViaCors() {
310         xsrfResourceFilter.setPluginModuleTracker(pluginModuleTracker(URI_ONE, true, false));
311 
312         when(request.getHeaderValue("Origin")).thenReturn(URI_ONE_CUSTOM_PORT);
313 
314         assertFalse(xsrfResourceFilter.passesAdditionalBrowserChecks(request));
315     }
316 
317     @Test
318     public void additionalBrowserChecksTakeHttpHttpsIntoAccountWhenCheckingAllowedViaCors() {
319         xsrfResourceFilter.setPluginModuleTracker(pluginModuleTracker(URI_ONE, true, false));
320 
321         when(request.getHeaderValue("Origin")).thenReturn(URI_ONE_HTTPS);
322 
323         assertFalse(xsrfResourceFilter.passesAdditionalBrowserChecks(request));
324     }
325 
326     /**
327      * Builds a plugin module tracker that contains no modules (and hence disallows all CORS requests).
328      *
329      * @return plugin module tracker with no modules
330      */
331     private PluginModuleTracker<CorsDefaults, CorsDefaultsModuleDescriptor> emptyPluginModuleTracker() {
332         final PluginModuleTracker<CorsDefaults, CorsDefaultsModuleDescriptor> pluginModuleTracker = mock(PluginModuleTracker.class);
333         when(pluginModuleTracker.getModules()).thenReturn(Collections.EMPTY_LIST);
334         return pluginModuleTracker;
335     }
336 
337     /**
338      * Builds a plugin module tracker containing a module that will allow/disallow a CORS request.
339      *
340      * @param originUri          the origin making the request
341      * @param originAllowed      true if the module should allow the origin
342      * @param credentialsAllowed true if the module should allow credentials to be included in requests from the origin
343      * @return plugin module tracker containing a module that allows/disallows requests from the origin
344      * based on the {@code originAllowed} and {@code credentialsAllowed} parameters
345      */
346     private PluginModuleTracker<CorsDefaults, CorsDefaultsModuleDescriptor> pluginModuleTracker(
347             final String originUri,
348             final boolean originAllowed,
349             final boolean credentialsAllowed) {
350         final CorsDefaults corsDefaults = mock(CorsDefaults.class);
351         when(corsDefaults.allowsOrigin(originUri)).thenReturn(originAllowed);
352         when(corsDefaults.allowsCredentials(originUri)).thenReturn(credentialsAllowed);
353 
354         final PluginModuleTracker<CorsDefaults, CorsDefaultsModuleDescriptor> pluginModuleTracker = mock(PluginModuleTracker.class);
355         when(pluginModuleTracker.getModules()).thenReturn(ImmutableList.of(corsDefaults));
356 
357         return pluginModuleTracker;
358     }
359 
360     private void setArbitraryCookie(final ContainerRequest request) {
361         when(request.getCookies()).thenReturn(ImmutableMap.of("foo", new javax.ws.rs.core.Cookie("bar", "baz")));
362     }
363 }