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
356
357
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
367
368
369
370
371
372
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 }