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
328
329
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
339
340
341
342
343
344
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 }