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