github.com/cjdelisle/matterfoss@v5.11.1+incompatible/web/handlers_test.go (about) 1 // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. 2 // See License.txt for license information. 3 4 package web 5 6 import ( 7 "net/http" 8 "net/http/httptest" 9 "testing" 10 11 "github.com/mattermost/mattermost-server/app" 12 "github.com/mattermost/mattermost-server/model" 13 "github.com/stretchr/testify/assert" 14 ) 15 16 func handlerForHTTPErrors(c *Context, w http.ResponseWriter, r *http.Request) { 17 c.Err = model.NewAppError("loginWithSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound) 18 } 19 20 func TestHandlerServeHTTPErrors(t *testing.T) { 21 th := Setup().InitBasic() 22 defer th.TearDown() 23 24 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 25 handler := web.NewHandler(handlerForHTTPErrors) 26 27 var flagtests = []struct { 28 name string 29 url string 30 mobile bool 31 redirect bool 32 }{ 33 {"redirect on desktop non-api endpoint", "/login/sso/saml", false, true}, 34 {"not redirect on desktop api endpoint", "/api/v4/test", false, false}, 35 {"not redirect on mobile non-api endpoint", "/login/sso/saml", true, false}, 36 {"not redirect on mobile api endpoint", "/api/v4/test", true, false}, 37 } 38 39 for _, tt := range flagtests { 40 t.Run(tt.name, func(t *testing.T) { 41 request := httptest.NewRequest("GET", tt.url, nil) 42 if tt.mobile { 43 request.Header.Add("X-Mobile-App", "mattermost") 44 } 45 response := httptest.NewRecorder() 46 handler.ServeHTTP(response, request) 47 48 if tt.redirect { 49 assert.Equal(t, response.Code, http.StatusFound) 50 } else { 51 assert.NotContains(t, response.Body.String(), "/error?message=") 52 } 53 }) 54 } 55 } 56 57 func handlerForHTTPSecureTransport(c *Context, w http.ResponseWriter, r *http.Request) { 58 } 59 60 func TestHandlerServeHTTPSecureTransport(t *testing.T) { 61 th := Setup().InitBasic() 62 defer th.TearDown() 63 64 th.App.UpdateConfig(func(config *model.Config) { 65 *config.ServiceSettings.TLSStrictTransport = true 66 *config.ServiceSettings.TLSStrictTransportMaxAge = 6000 67 }) 68 69 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 70 handler := web.NewHandler(handlerForHTTPSecureTransport) 71 72 request := httptest.NewRequest("GET", "/api/v4/test", nil) 73 74 response := httptest.NewRecorder() 75 handler.ServeHTTP(response, request) 76 header := response.Header().Get("Strict-Transport-Security") 77 78 if header == "" { 79 t.Errorf("Strict-Transport-Security expected but not existent") 80 } 81 82 if header != "max-age=6000" { 83 t.Errorf("Expected max-age=6000, got %s", header) 84 } 85 86 th.App.UpdateConfig(func(config *model.Config) { 87 *config.ServiceSettings.TLSStrictTransport = false 88 }) 89 90 request = httptest.NewRequest("GET", "/api/v4/test", nil) 91 92 response = httptest.NewRecorder() 93 handler.ServeHTTP(response, request) 94 header = response.Header().Get("Strict-Transport-Security") 95 96 if header != "" { 97 t.Errorf("Strict-Transport-Security header is not expected, but returned") 98 } 99 } 100 101 func handlerForCSRFToken(c *Context, w http.ResponseWriter, r *http.Request) { 102 } 103 104 func TestHandlerServeCSRFToken(t *testing.T) { 105 th := Setup().InitBasic() 106 defer th.TearDown() 107 108 session := &model.Session{ 109 UserId: th.BasicUser.Id, 110 CreateAt: model.GetMillis(), 111 Roles: model.SYSTEM_USER_ROLE_ID, 112 IsOAuth: false, 113 } 114 session.GenerateCSRF() 115 session.SetExpireInDays(1) 116 session, err := th.App.CreateSession(session) 117 if err != nil { 118 t.Errorf("Expected nil, got %s", err) 119 } 120 121 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 122 123 handler := Handler{ 124 GetGlobalAppOptions: web.GetGlobalAppOptions, 125 HandleFunc: handlerForCSRFToken, 126 RequireSession: true, 127 TrustRequester: false, 128 RequireMfa: false, 129 IsStatic: false, 130 } 131 132 cookie := &http.Cookie{ 133 Name: model.SESSION_COOKIE_USER, 134 Value: th.BasicUser.Username, 135 } 136 cookie2 := &http.Cookie{ 137 Name: model.SESSION_COOKIE_TOKEN, 138 Value: session.Token, 139 } 140 cookie3 := &http.Cookie{ 141 Name: model.SESSION_COOKIE_CSRF, 142 Value: session.GetCSRF(), 143 } 144 145 // CSRF Token Used - Success Expected 146 147 request := httptest.NewRequest("POST", "/api/v4/test", nil) 148 request.AddCookie(cookie) 149 request.AddCookie(cookie2) 150 request.AddCookie(cookie3) 151 request.Header.Add(model.HEADER_CSRF_TOKEN, session.GetCSRF()) 152 response := httptest.NewRecorder() 153 handler.ServeHTTP(response, request) 154 155 if response.Code != 200 { 156 t.Errorf("Expected status 200, got %d", response.Code) 157 } 158 159 // No CSRF Token Used - Failure Expected 160 161 request = httptest.NewRequest("POST", "/api/v4/test", nil) 162 request.AddCookie(cookie) 163 request.AddCookie(cookie2) 164 request.AddCookie(cookie3) 165 response = httptest.NewRecorder() 166 handler.ServeHTTP(response, request) 167 168 if response.Code != 401 { 169 t.Errorf("Expected status 401, got %d", response.Code) 170 } 171 172 // Fallback Behavior Used - Success expected 173 // ToDo (DSchalla) 2019/01/04: Remove once legacy CSRF Handling is removed 174 th.App.UpdateConfig(func(config *model.Config) { 175 *config.ServiceSettings.ExperimentalStrictCSRFEnforcement = false 176 }) 177 request = httptest.NewRequest("POST", "/api/v4/test", nil) 178 request.AddCookie(cookie) 179 request.AddCookie(cookie2) 180 request.AddCookie(cookie3) 181 request.Header.Add(model.HEADER_REQUESTED_WITH, model.HEADER_REQUESTED_WITH_XML) 182 response = httptest.NewRecorder() 183 handler.ServeHTTP(response, request) 184 185 if response.Code != 200 { 186 t.Errorf("Expected status 200, got %d", response.Code) 187 } 188 189 // Fallback Behavior Used with Strict Enforcement - Failure Expected 190 // ToDo (DSchalla) 2019/01/04: Remove once legacy CSRF Handling is removed 191 th.App.UpdateConfig(func(config *model.Config) { 192 *config.ServiceSettings.ExperimentalStrictCSRFEnforcement = true 193 }) 194 response = httptest.NewRecorder() 195 handler.ServeHTTP(response, request) 196 197 if response.Code != 401 { 198 t.Errorf("Expected status 200, got %d", response.Code) 199 } 200 } 201 202 func handlerForCSPHeader(c *Context, w http.ResponseWriter, r *http.Request) { 203 } 204 205 func TestHandlerServeCSPHeader(t *testing.T) { 206 t.Run("non-static", func(t *testing.T) { 207 th := Setup().InitBasic() 208 defer th.TearDown() 209 210 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 211 212 handler := Handler{ 213 GetGlobalAppOptions: web.GetGlobalAppOptions, 214 HandleFunc: handlerForCSPHeader, 215 RequireSession: false, 216 TrustRequester: false, 217 RequireMfa: false, 218 IsStatic: false, 219 } 220 221 request := httptest.NewRequest("POST", "/api/v4/test", nil) 222 response := httptest.NewRecorder() 223 handler.ServeHTTP(response, request) 224 assert.Equal(t, 200, response.Code) 225 assert.Empty(t, response.Header()["Content-Security-Policy"]) 226 }) 227 228 t.Run("static, without subpath", func(t *testing.T) { 229 th := Setup().InitBasic() 230 defer th.TearDown() 231 232 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 233 234 handler := Handler{ 235 GetGlobalAppOptions: web.GetGlobalAppOptions, 236 HandleFunc: handlerForCSPHeader, 237 RequireSession: false, 238 TrustRequester: false, 239 RequireMfa: false, 240 IsStatic: true, 241 } 242 243 request := httptest.NewRequest("POST", "/", nil) 244 response := httptest.NewRecorder() 245 handler.ServeHTTP(response, request) 246 assert.Equal(t, 200, response.Code) 247 assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/"}) 248 }) 249 250 t.Run("static, with subpath", func(t *testing.T) { 251 th := Setup().InitBasic() 252 defer th.TearDown() 253 254 th.App.UpdateConfig(func(cfg *model.Config) { 255 *cfg.ServiceSettings.SiteURL = *cfg.ServiceSettings.SiteURL + "/subpath" 256 }) 257 258 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 259 260 handler := Handler{ 261 GetGlobalAppOptions: web.GetGlobalAppOptions, 262 HandleFunc: handlerForCSPHeader, 263 RequireSession: false, 264 TrustRequester: false, 265 RequireMfa: false, 266 IsStatic: true, 267 } 268 269 request := httptest.NewRequest("POST", "/", nil) 270 response := httptest.NewRecorder() 271 handler.ServeHTTP(response, request) 272 assert.Equal(t, 200, response.Code) 273 assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/"}) 274 275 // TODO: It's hard to unit test this now that the CSP directive is effectively 276 // decided in Setup(). Circle back to this in master once the memory store is 277 // merged, allowing us to mock the desired initial config to take effect in Setup(). 278 // assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/ 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='") 279 280 th.App.UpdateConfig(func(cfg *model.Config) { 281 *cfg.ServiceSettings.SiteURL = *cfg.ServiceSettings.SiteURL + "/subpath2" 282 }) 283 284 request = httptest.NewRequest("POST", "/", nil) 285 response = httptest.NewRecorder() 286 handler.ServeHTTP(response, request) 287 assert.Equal(t, 200, response.Code) 288 assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/"}) 289 // TODO: See above. 290 // assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.segment.com/analytics.js/ 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed") 291 }) 292 } 293 func TestCheckCSRFToken(t *testing.T) { 294 t.Run("should allow a POST request with a valid CSRF token header", func(t *testing.T) { 295 th := Setup() 296 defer th.TearDown() 297 298 h := &Handler{ 299 RequireSession: true, 300 TrustRequester: false, 301 } 302 303 token := "token" 304 tokenLocation := app.TokenLocationCookie 305 306 c := &Context{ 307 App: th.App, 308 } 309 r, _ := http.NewRequest(http.MethodPost, "", nil) 310 r.Header.Set(model.HEADER_CSRF_TOKEN, token) 311 session := &model.Session{ 312 Props: map[string]string{ 313 "csrf": token, 314 }, 315 } 316 317 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 318 319 assert.True(t, checked) 320 assert.True(t, passed) 321 assert.Nil(t, c.Err) 322 }) 323 324 t.Run("should allow a POST request with an X-Requested-With header", func(t *testing.T) { 325 th := Setup() 326 defer th.TearDown() 327 328 h := &Handler{ 329 RequireSession: true, 330 TrustRequester: false, 331 } 332 333 token := "token" 334 tokenLocation := app.TokenLocationCookie 335 336 c := &Context{ 337 App: th.App, 338 Log: th.App.Log, 339 } 340 r, _ := http.NewRequest(http.MethodPost, "", nil) 341 r.Header.Set(model.HEADER_REQUESTED_WITH, model.HEADER_REQUESTED_WITH_XML) 342 session := &model.Session{ 343 Props: map[string]string{ 344 "csrf": token, 345 }, 346 } 347 348 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 349 350 assert.True(t, checked) 351 assert.True(t, passed) 352 assert.Nil(t, c.Err) 353 }) 354 355 t.Run("should not allow a POST request with an X-Requested-With header with strict CSRF enforcement enabled", func(t *testing.T) { 356 th := Setup() 357 defer th.TearDown() 358 359 th.App.UpdateConfig(func(cfg *model.Config) { 360 *cfg.ServiceSettings.ExperimentalStrictCSRFEnforcement = true 361 }) 362 363 h := &Handler{ 364 RequireSession: true, 365 TrustRequester: false, 366 } 367 368 token := "token" 369 tokenLocation := app.TokenLocationCookie 370 371 c := &Context{ 372 App: th.App, 373 Log: th.App.Log, 374 } 375 r, _ := http.NewRequest(http.MethodPost, "", nil) 376 r.Header.Set(model.HEADER_REQUESTED_WITH, model.HEADER_REQUESTED_WITH_XML) 377 session := &model.Session{ 378 Props: map[string]string{ 379 "csrf": token, 380 }, 381 } 382 383 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 384 385 assert.True(t, checked) 386 assert.False(t, passed) 387 assert.NotNil(t, c.Err) 388 }) 389 390 t.Run("should not allow a POST request without either header", func(t *testing.T) { 391 th := Setup() 392 defer th.TearDown() 393 394 h := &Handler{ 395 RequireSession: true, 396 TrustRequester: false, 397 } 398 399 token := "token" 400 tokenLocation := app.TokenLocationCookie 401 402 c := &Context{ 403 App: th.App, 404 } 405 r, _ := http.NewRequest(http.MethodPost, "", nil) 406 session := &model.Session{ 407 Props: map[string]string{ 408 "csrf": token, 409 }, 410 } 411 412 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 413 414 assert.True(t, checked) 415 assert.False(t, passed) 416 assert.NotNil(t, c.Err) 417 }) 418 419 t.Run("should not check GET requests", func(t *testing.T) { 420 th := Setup() 421 defer th.TearDown() 422 423 h := &Handler{ 424 RequireSession: true, 425 TrustRequester: false, 426 } 427 428 token := "token" 429 tokenLocation := app.TokenLocationCookie 430 431 c := &Context{ 432 App: th.App, 433 } 434 r, _ := http.NewRequest(http.MethodGet, "", nil) 435 436 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, nil) 437 438 assert.False(t, checked) 439 assert.False(t, passed) 440 assert.Nil(t, c.Err) 441 }) 442 443 t.Run("should not check a request passing the auth token in a header", func(t *testing.T) { 444 th := Setup() 445 defer th.TearDown() 446 447 h := &Handler{ 448 RequireSession: true, 449 TrustRequester: false, 450 } 451 452 token := "token" 453 tokenLocation := app.TokenLocationHeader 454 455 c := &Context{ 456 App: th.App, 457 } 458 r, _ := http.NewRequest(http.MethodPost, "", nil) 459 460 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, nil) 461 462 assert.False(t, checked) 463 assert.False(t, passed) 464 assert.Nil(t, c.Err) 465 }) 466 }