github.com/adacta-ru/mattermost-server/v6@v6.0.0/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/adacta-ru/mattermost-server/v6/app" 12 "github.com/adacta-ru/mattermost-server/v6/model" 13 "github.com/adacta-ru/mattermost-server/v6/plugin/plugintest/mock" 14 "github.com/adacta-ru/mattermost-server/v6/store/storetest/mocks" 15 "github.com/stretchr/testify/assert" 16 "github.com/stretchr/testify/require" 17 ) 18 19 func handlerForHTTPErrors(c *Context, w http.ResponseWriter, r *http.Request) { 20 c.Err = model.NewAppError("loginWithSaml", "api.user.saml.not_available.app_error", nil, "", http.StatusFound) 21 } 22 23 func TestHandlerServeHTTPErrors(t *testing.T) { 24 th := SetupWithStoreMock(t) 25 defer th.TearDown() 26 27 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 28 handler := web.NewHandler(handlerForHTTPErrors) 29 30 var flagtests = []struct { 31 name string 32 url string 33 mobile bool 34 redirect bool 35 }{ 36 {"redirect on desktop non-api endpoint", "/login/sso/saml", false, true}, 37 {"not redirect on desktop api endpoint", "/api/v4/test", false, false}, 38 {"not redirect on mobile non-api endpoint", "/login/sso/saml", true, false}, 39 {"not redirect on mobile api endpoint", "/api/v4/test", true, false}, 40 } 41 42 for _, tt := range flagtests { 43 t.Run(tt.name, func(t *testing.T) { 44 request := httptest.NewRequest("GET", tt.url, nil) 45 if tt.mobile { 46 request.Header.Add("X-Mobile-App", "mattermost") 47 } 48 response := httptest.NewRecorder() 49 handler.ServeHTTP(response, request) 50 51 if tt.redirect { 52 assert.Equal(t, response.Code, http.StatusFound) 53 } else { 54 assert.NotContains(t, response.Body.String(), "/error?message=") 55 } 56 }) 57 } 58 } 59 60 func handlerForHTTPSecureTransport(c *Context, w http.ResponseWriter, r *http.Request) { 61 } 62 63 func TestHandlerServeHTTPSecureTransport(t *testing.T) { 64 th := SetupWithStoreMock(t) 65 defer th.TearDown() 66 67 mockStore := th.App.Srv().Store.(*mocks.Store) 68 mockUserStore := mocks.UserStore{} 69 mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) 70 mockPostStore := mocks.PostStore{} 71 mockPostStore.On("GetMaxPostSize").Return(65535, nil) 72 mockSystemStore := mocks.SystemStore{} 73 mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil) 74 mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil) 75 mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil) 76 77 mockStore.On("User").Return(&mockUserStore) 78 mockStore.On("Post").Return(&mockPostStore) 79 mockStore.On("System").Return(&mockSystemStore) 80 81 th.App.UpdateConfig(func(config *model.Config) { 82 *config.ServiceSettings.TLSStrictTransport = true 83 *config.ServiceSettings.TLSStrictTransportMaxAge = 6000 84 }) 85 86 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 87 handler := web.NewHandler(handlerForHTTPSecureTransport) 88 89 request := httptest.NewRequest("GET", "/api/v4/test", nil) 90 91 response := httptest.NewRecorder() 92 handler.ServeHTTP(response, request) 93 header := response.Header().Get("Strict-Transport-Security") 94 95 if header == "" { 96 t.Errorf("Strict-Transport-Security expected but not existent") 97 } 98 99 if header != "max-age=6000" { 100 t.Errorf("Expected max-age=6000, got %s", header) 101 } 102 103 th.App.UpdateConfig(func(config *model.Config) { 104 *config.ServiceSettings.TLSStrictTransport = false 105 }) 106 107 request = httptest.NewRequest("GET", "/api/v4/test", nil) 108 109 response = httptest.NewRecorder() 110 handler.ServeHTTP(response, request) 111 header = response.Header().Get("Strict-Transport-Security") 112 113 if header != "" { 114 t.Errorf("Strict-Transport-Security header is not expected, but returned") 115 } 116 } 117 118 func handlerForCSRFToken(c *Context, w http.ResponseWriter, r *http.Request) { 119 } 120 121 func TestHandlerServeCSRFToken(t *testing.T) { 122 th := Setup(t).InitBasic() 123 defer th.TearDown() 124 125 session := &model.Session{ 126 UserId: th.BasicUser.Id, 127 CreateAt: model.GetMillis(), 128 Roles: model.SYSTEM_USER_ROLE_ID, 129 IsOAuth: false, 130 } 131 session.GenerateCSRF() 132 th.App.SetSessionExpireInDays(session, 1) 133 session, err := th.App.CreateSession(session) 134 if err != nil { 135 t.Errorf("Expected nil, got %s", err) 136 } 137 138 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 139 140 handler := Handler{ 141 GetGlobalAppOptions: web.GetGlobalAppOptions, 142 HandleFunc: handlerForCSRFToken, 143 RequireSession: true, 144 TrustRequester: false, 145 RequireMfa: false, 146 IsStatic: false, 147 } 148 149 cookie := &http.Cookie{ 150 Name: model.SESSION_COOKIE_USER, 151 Value: th.BasicUser.Username, 152 } 153 cookie2 := &http.Cookie{ 154 Name: model.SESSION_COOKIE_TOKEN, 155 Value: session.Token, 156 } 157 cookie3 := &http.Cookie{ 158 Name: model.SESSION_COOKIE_CSRF, 159 Value: session.GetCSRF(), 160 } 161 162 // CSRF Token Used - Success Expected 163 164 request := httptest.NewRequest("POST", "/api/v4/test", nil) 165 request.AddCookie(cookie) 166 request.AddCookie(cookie2) 167 request.AddCookie(cookie3) 168 request.Header.Add(model.HEADER_CSRF_TOKEN, session.GetCSRF()) 169 response := httptest.NewRecorder() 170 handler.ServeHTTP(response, request) 171 172 if response.Code != 200 { 173 t.Errorf("Expected status 200, got %d", response.Code) 174 } 175 176 // No CSRF Token Used - Failure Expected 177 178 request = httptest.NewRequest("POST", "/api/v4/test", nil) 179 request.AddCookie(cookie) 180 request.AddCookie(cookie2) 181 request.AddCookie(cookie3) 182 response = httptest.NewRecorder() 183 handler.ServeHTTP(response, request) 184 185 if response.Code != 401 { 186 t.Errorf("Expected status 401, got %d", response.Code) 187 } 188 189 // Fallback Behavior Used - Success 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 = false 193 }) 194 request = httptest.NewRequest("POST", "/api/v4/test", nil) 195 request.AddCookie(cookie) 196 request.AddCookie(cookie2) 197 request.AddCookie(cookie3) 198 request.Header.Add(model.HEADER_REQUESTED_WITH, model.HEADER_REQUESTED_WITH_XML) 199 response = httptest.NewRecorder() 200 handler.ServeHTTP(response, request) 201 202 if response.Code != 200 { 203 t.Errorf("Expected status 200, got %d", response.Code) 204 } 205 206 // Fallback Behavior Used with Strict Enforcement - Failure Expected 207 // ToDo (DSchalla) 2019/01/04: Remove once legacy CSRF Handling is removed 208 th.App.UpdateConfig(func(config *model.Config) { 209 *config.ServiceSettings.ExperimentalStrictCSRFEnforcement = true 210 }) 211 response = httptest.NewRecorder() 212 handler.ServeHTTP(response, request) 213 214 if response.Code != 401 { 215 t.Errorf("Expected status 200, got %d", response.Code) 216 } 217 218 // Handler with RequireSession set to false 219 220 handlerNoSession := Handler{ 221 GetGlobalAppOptions: web.GetGlobalAppOptions, 222 HandleFunc: handlerForCSRFToken, 223 RequireSession: false, 224 TrustRequester: false, 225 RequireMfa: false, 226 IsStatic: false, 227 } 228 229 // CSRF Token Used - Success Expected 230 231 request = httptest.NewRequest("POST", "/api/v4/test", nil) 232 request.AddCookie(cookie) 233 request.AddCookie(cookie2) 234 request.AddCookie(cookie3) 235 request.Header.Add(model.HEADER_CSRF_TOKEN, session.GetCSRF()) 236 response = httptest.NewRecorder() 237 handlerNoSession.ServeHTTP(response, request) 238 239 if response.Code != 200 { 240 t.Errorf("Expected status 200, got %d", response.Code) 241 } 242 243 // No CSRF Token Used - Failure Expected 244 245 request = httptest.NewRequest("POST", "/api/v4/test", nil) 246 request.AddCookie(cookie) 247 request.AddCookie(cookie2) 248 request.AddCookie(cookie3) 249 response = httptest.NewRecorder() 250 handlerNoSession.ServeHTTP(response, request) 251 252 if response.Code != 401 { 253 t.Errorf("Expected status 401, got %d", response.Code) 254 } 255 } 256 257 func handlerForCSPHeader(c *Context, w http.ResponseWriter, r *http.Request) { 258 } 259 260 func TestHandlerServeCSPHeader(t *testing.T) { 261 t.Run("non-static", func(t *testing.T) { 262 th := SetupWithStoreMock(t) 263 defer th.TearDown() 264 265 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 266 267 handler := Handler{ 268 GetGlobalAppOptions: web.GetGlobalAppOptions, 269 HandleFunc: handlerForCSPHeader, 270 RequireSession: false, 271 TrustRequester: false, 272 RequireMfa: false, 273 IsStatic: false, 274 } 275 276 request := httptest.NewRequest("POST", "/api/v4/test", nil) 277 response := httptest.NewRecorder() 278 handler.ServeHTTP(response, request) 279 assert.Equal(t, 200, response.Code) 280 assert.Empty(t, response.Header()["Content-Security-Policy"]) 281 }) 282 283 t.Run("static, without subpath", func(t *testing.T) { 284 th := SetupWithStoreMock(t) 285 defer th.TearDown() 286 287 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 288 289 handler := Handler{ 290 GetGlobalAppOptions: web.GetGlobalAppOptions, 291 HandleFunc: handlerForCSPHeader, 292 RequireSession: false, 293 TrustRequester: false, 294 RequireMfa: false, 295 IsStatic: true, 296 } 297 298 request := httptest.NewRequest("POST", "/", nil) 299 response := httptest.NewRecorder() 300 handler.ServeHTTP(response, request) 301 assert.Equal(t, 200, response.Code) 302 assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com"}) 303 }) 304 305 t.Run("static, with subpath", func(t *testing.T) { 306 th := SetupWithStoreMock(t) 307 defer th.TearDown() 308 309 mockStore := th.App.Srv().Store.(*mocks.Store) 310 mockUserStore := mocks.UserStore{} 311 mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) 312 mockPostStore := mocks.PostStore{} 313 mockPostStore.On("GetMaxPostSize").Return(65535, nil) 314 mockSystemStore := mocks.SystemStore{} 315 mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil) 316 mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil) 317 mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil) 318 319 mockStore.On("User").Return(&mockUserStore) 320 mockStore.On("Post").Return(&mockPostStore) 321 mockStore.On("System").Return(&mockSystemStore) 322 323 th.App.UpdateConfig(func(cfg *model.Config) { 324 *cfg.ServiceSettings.SiteURL = *cfg.ServiceSettings.SiteURL + "/subpath" 325 }) 326 327 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 328 329 handler := Handler{ 330 GetGlobalAppOptions: web.GetGlobalAppOptions, 331 HandleFunc: handlerForCSPHeader, 332 RequireSession: false, 333 TrustRequester: false, 334 RequireMfa: false, 335 IsStatic: true, 336 } 337 338 request := httptest.NewRequest("POST", "/", nil) 339 response := httptest.NewRecorder() 340 handler.ServeHTTP(response, request) 341 assert.Equal(t, 200, response.Code) 342 assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com"}) 343 344 // TODO: It's hard to unit test this now that the CSP directive is effectively 345 // decided in Setup(). Circle back to this in master once the memory store is 346 // merged, allowing us to mock the desired initial config to take effect in Setup(). 347 // assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='") 348 349 th.App.UpdateConfig(func(cfg *model.Config) { 350 *cfg.ServiceSettings.SiteURL = *cfg.ServiceSettings.SiteURL + "/subpath2" 351 }) 352 353 request = httptest.NewRequest("POST", "/", nil) 354 response = httptest.NewRecorder() 355 handler.ServeHTTP(response, request) 356 assert.Equal(t, 200, response.Code) 357 assert.Equal(t, response.Header()["Content-Security-Policy"], []string{"frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com"}) 358 // TODO: See above. 359 // assert.Contains(t, response.Header()["Content-Security-Policy"], "frame-ancestors 'self'; script-src 'self' cdn.rudderlabs.com 'sha256-tPOjw+tkVs9axL78ZwGtYl975dtyPHB6LYKAO2R3gR4='", "csp header incorrectly changed after subpath changed") 360 }) 361 } 362 363 func TestHandlerServeInvalidToken(t *testing.T) { 364 testCases := []struct { 365 Description string 366 SiteURL string 367 ExpectedSetCookieHeaderRegexp string 368 }{ 369 {"no subpath", "http://localhost:8065", "^MMAUTHTOKEN=; Path=/"}, 370 {"subpath", "http://localhost:8065/subpath", "^MMAUTHTOKEN=; Path=/subpath"}, 371 } 372 373 for _, tc := range testCases { 374 t.Run(tc.Description, func(t *testing.T) { 375 th := Setup(t) 376 defer th.TearDown() 377 378 th.App.UpdateConfig(func(cfg *model.Config) { 379 *cfg.ServiceSettings.SiteURL = tc.SiteURL 380 }) 381 382 web := New(th.Server, th.Server.AppOptions, th.Server.Router) 383 384 handler := Handler{ 385 GetGlobalAppOptions: web.GetGlobalAppOptions, 386 HandleFunc: handlerForCSRFToken, 387 RequireSession: true, 388 TrustRequester: false, 389 RequireMfa: false, 390 IsStatic: false, 391 } 392 393 cookie := &http.Cookie{ 394 Name: model.SESSION_COOKIE_TOKEN, 395 Value: "invalid", 396 } 397 398 request := httptest.NewRequest("POST", "/api/v4/test", nil) 399 request.AddCookie(cookie) 400 response := httptest.NewRecorder() 401 handler.ServeHTTP(response, request) 402 require.Equal(t, http.StatusUnauthorized, response.Code) 403 404 cookies := response.Header().Get("Set-Cookie") 405 assert.Regexp(t, tc.ExpectedSetCookieHeaderRegexp, cookies) 406 }) 407 } 408 } 409 410 func TestCheckCSRFToken(t *testing.T) { 411 t.Run("should allow a POST request with a valid CSRF token header", func(t *testing.T) { 412 th := SetupWithStoreMock(t) 413 defer th.TearDown() 414 415 h := &Handler{ 416 RequireSession: true, 417 TrustRequester: false, 418 } 419 420 token := "token" 421 tokenLocation := app.TokenLocationCookie 422 423 c := &Context{ 424 App: th.App, 425 } 426 r, _ := http.NewRequest(http.MethodPost, "", nil) 427 r.Header.Set(model.HEADER_CSRF_TOKEN, token) 428 session := &model.Session{ 429 Props: map[string]string{ 430 "csrf": token, 431 }, 432 } 433 434 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 435 436 assert.True(t, checked) 437 assert.True(t, passed) 438 assert.Nil(t, c.Err) 439 }) 440 441 t.Run("should allow a POST request with an X-Requested-With header", func(t *testing.T) { 442 th := SetupWithStoreMock(t) 443 defer th.TearDown() 444 445 h := &Handler{ 446 RequireSession: true, 447 TrustRequester: false, 448 } 449 450 token := "token" 451 tokenLocation := app.TokenLocationCookie 452 453 c := &Context{ 454 App: th.App, 455 Log: th.App.Log(), 456 } 457 r, _ := http.NewRequest(http.MethodPost, "", nil) 458 r.Header.Set(model.HEADER_REQUESTED_WITH, model.HEADER_REQUESTED_WITH_XML) 459 session := &model.Session{ 460 Props: map[string]string{ 461 "csrf": token, 462 }, 463 } 464 465 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 466 467 assert.True(t, checked) 468 assert.True(t, passed) 469 assert.Nil(t, c.Err) 470 }) 471 472 t.Run("should not allow a POST request with an X-Requested-With header with strict CSRF enforcement enabled", func(t *testing.T) { 473 th := SetupWithStoreMock(t) 474 defer th.TearDown() 475 476 mockStore := th.App.Srv().Store.(*mocks.Store) 477 mockUserStore := mocks.UserStore{} 478 mockUserStore.On("Count", mock.Anything).Return(int64(10), nil) 479 mockPostStore := mocks.PostStore{} 480 mockPostStore.On("GetMaxPostSize").Return(65535, nil) 481 mockSystemStore := mocks.SystemStore{} 482 mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil) 483 mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil) 484 mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil) 485 486 mockStore.On("User").Return(&mockUserStore) 487 mockStore.On("Post").Return(&mockPostStore) 488 mockStore.On("System").Return(&mockSystemStore) 489 490 th.App.UpdateConfig(func(cfg *model.Config) { 491 *cfg.ServiceSettings.ExperimentalStrictCSRFEnforcement = true 492 }) 493 494 h := &Handler{ 495 RequireSession: true, 496 TrustRequester: false, 497 } 498 499 token := "token" 500 tokenLocation := app.TokenLocationCookie 501 502 c := &Context{ 503 App: th.App, 504 Log: th.App.Log(), 505 } 506 r, _ := http.NewRequest(http.MethodPost, "", nil) 507 r.Header.Set(model.HEADER_REQUESTED_WITH, model.HEADER_REQUESTED_WITH_XML) 508 session := &model.Session{ 509 Props: map[string]string{ 510 "csrf": token, 511 }, 512 } 513 514 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 515 516 assert.True(t, checked) 517 assert.False(t, passed) 518 assert.NotNil(t, c.Err) 519 }) 520 521 t.Run("should not allow a POST request without either header", func(t *testing.T) { 522 th := SetupWithStoreMock(t) 523 defer th.TearDown() 524 525 h := &Handler{ 526 RequireSession: true, 527 TrustRequester: false, 528 } 529 530 token := "token" 531 tokenLocation := app.TokenLocationCookie 532 533 c := &Context{ 534 App: th.App, 535 } 536 r, _ := http.NewRequest(http.MethodPost, "", nil) 537 session := &model.Session{ 538 Props: map[string]string{ 539 "csrf": token, 540 }, 541 } 542 543 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 544 545 assert.True(t, checked) 546 assert.False(t, passed) 547 assert.NotNil(t, c.Err) 548 }) 549 550 t.Run("should not check GET requests", func(t *testing.T) { 551 th := SetupWithStoreMock(t) 552 defer th.TearDown() 553 554 h := &Handler{ 555 RequireSession: true, 556 TrustRequester: false, 557 } 558 559 token := "token" 560 tokenLocation := app.TokenLocationCookie 561 562 c := &Context{ 563 App: th.App, 564 } 565 r, _ := http.NewRequest(http.MethodGet, "", nil) 566 session := &model.Session{ 567 Props: map[string]string{ 568 "csrf": token, 569 }, 570 } 571 572 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 573 574 assert.False(t, checked) 575 assert.False(t, passed) 576 assert.Nil(t, c.Err) 577 }) 578 579 t.Run("should not check a request passing the auth token in a header", func(t *testing.T) { 580 th := SetupWithStoreMock(t) 581 defer th.TearDown() 582 583 h := &Handler{ 584 RequireSession: true, 585 TrustRequester: false, 586 } 587 588 token := "token" 589 tokenLocation := app.TokenLocationHeader 590 591 c := &Context{ 592 App: th.App, 593 } 594 r, _ := http.NewRequest(http.MethodPost, "", nil) 595 session := &model.Session{ 596 Props: map[string]string{ 597 "csrf": token, 598 }, 599 } 600 601 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 602 603 assert.False(t, checked) 604 assert.False(t, passed) 605 assert.Nil(t, c.Err) 606 }) 607 608 t.Run("should not check a request passing a nil session", func(t *testing.T) { 609 th := SetupWithStoreMock(t) 610 defer th.TearDown() 611 612 h := &Handler{ 613 RequireSession: false, 614 TrustRequester: false, 615 } 616 617 token := "token" 618 tokenLocation := app.TokenLocationCookie 619 620 c := &Context{ 621 App: th.App, 622 } 623 r, _ := http.NewRequest(http.MethodPost, "", nil) 624 r.Header.Set(model.HEADER_CSRF_TOKEN, token) 625 626 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, nil) 627 628 assert.False(t, checked) 629 assert.False(t, passed) 630 assert.Nil(t, c.Err) 631 }) 632 633 t.Run("should check requests for handlers that don't require a session but have one", func(t *testing.T) { 634 th := SetupWithStoreMock(t) 635 defer th.TearDown() 636 637 h := &Handler{ 638 RequireSession: false, 639 TrustRequester: false, 640 } 641 642 token := "token" 643 tokenLocation := app.TokenLocationCookie 644 645 c := &Context{ 646 App: th.App, 647 } 648 r, _ := http.NewRequest(http.MethodPost, "", nil) 649 r.Header.Set(model.HEADER_CSRF_TOKEN, token) 650 session := &model.Session{ 651 Props: map[string]string{ 652 "csrf": token, 653 }, 654 } 655 656 checked, passed := h.checkCSRFToken(c, r, token, tokenLocation, session) 657 658 assert.True(t, checked) 659 assert.True(t, passed) 660 assert.Nil(t, c.Err) 661 }) 662 }