diff --git a/server/api/api.go b/server/api/api.go index 62f3690a4..017487ca9 100644 --- a/server/api/api.go +++ b/server/api/api.go @@ -18,6 +18,11 @@ import ( "github.com/mattermost/focalboard/server/utils" ) +const ( + HEADER_REQUESTED_WITH = "X-Requested-With" + HEADER_REQUESTED_WITH_XML = "XMLHttpRequest" +) + // ---------------------------------------------------------------------------------------------------- // REST APIs @@ -35,35 +40,64 @@ func (a *API) app() *app.App { } func (a *API) RegisterRoutes(r *mux.Router) { - r.HandleFunc("/api/v1/blocks", a.sessionRequired(a.handleGetBlocks)).Methods("GET") - r.HandleFunc("/api/v1/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST") - r.HandleFunc("/api/v1/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE") - r.HandleFunc("/api/v1/blocks/{blockID}/subtree", a.attachSession(a.handleGetSubTree, false)).Methods("GET") + apiv1 := r.PathPrefix("/api/v1").Subrouter() + apiv1.Use(a.requireCSRFToken) - r.HandleFunc("/api/v1/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET") - r.HandleFunc("/api/v1/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET") - r.HandleFunc("/api/v1/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST") + apiv1.HandleFunc("/blocks", a.sessionRequired(a.handleGetBlocks)).Methods("GET") + apiv1.HandleFunc("/blocks", a.sessionRequired(a.handlePostBlocks)).Methods("POST") + apiv1.HandleFunc("/blocks/{blockID}", a.sessionRequired(a.handleDeleteBlock)).Methods("DELETE") + apiv1.HandleFunc("/blocks/{blockID}/subtree", a.attachSession(a.handleGetSubTree, false)).Methods("GET") - r.HandleFunc("/api/v1/login", a.handleLogin).Methods("POST") - r.HandleFunc("/api/v1/register", a.handleRegister).Methods("POST") + apiv1.HandleFunc("/users/me", a.sessionRequired(a.handleGetMe)).Methods("GET") + apiv1.HandleFunc("/users/{userID}", a.sessionRequired(a.handleGetUser)).Methods("GET") + apiv1.HandleFunc("/users/{userID}/changepassword", a.sessionRequired(a.handleChangePassword)).Methods("POST") - r.HandleFunc("/api/v1/files", a.sessionRequired(a.handleUploadFile)).Methods("POST") - r.HandleFunc("/files/{filename}", a.sessionRequired(a.handleServeFile)).Methods("GET") + apiv1.HandleFunc("/login", a.handleLogin).Methods("POST") + apiv1.HandleFunc("/register", a.handleRegister).Methods("POST") - r.HandleFunc("/api/v1/blocks/export", a.sessionRequired(a.handleExport)).Methods("GET") - r.HandleFunc("/api/v1/blocks/import", a.sessionRequired(a.handleImport)).Methods("POST") + apiv1.HandleFunc("/files", a.sessionRequired(a.handleUploadFile)).Methods("POST") - r.HandleFunc("/api/v1/sharing/{rootID}", a.sessionRequired(a.handlePostSharing)).Methods("POST") - r.HandleFunc("/api/v1/sharing/{rootID}", a.sessionRequired(a.handleGetSharing)).Methods("GET") + apiv1.HandleFunc("/blocks/export", a.sessionRequired(a.handleExport)).Methods("GET") + apiv1.HandleFunc("/blocks/import", a.sessionRequired(a.handleImport)).Methods("POST") - r.HandleFunc("/api/v1/workspace", a.sessionRequired(a.handleGetWorkspace)).Methods("GET") - r.HandleFunc("/api/v1/workspace/regenerate_signup_token", a.sessionRequired(a.handlePostWorkspaceRegenerateSignupToken)).Methods("POST") + apiv1.HandleFunc("/sharing/{rootID}", a.sessionRequired(a.handlePostSharing)).Methods("POST") + apiv1.HandleFunc("/sharing/{rootID}", a.sessionRequired(a.handleGetSharing)).Methods("GET") + + apiv1.HandleFunc("/workspace", a.sessionRequired(a.handleGetWorkspace)).Methods("GET") + apiv1.HandleFunc("/workspace/regenerate_signup_token", a.sessionRequired(a.handlePostWorkspaceRegenerateSignupToken)).Methods("POST") + + // Get Files API + + files := r.PathPrefix("/files").Subrouter() + files.HandleFunc("/{filename}", a.sessionRequired(a.handleServeFile)).Methods("GET") } func (a *API) RegisterAdminRoutes(r *mux.Router) { r.HandleFunc("/api/v1/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST") } +func (a *API) requireCSRFToken(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !a.checkCSRFToken(r) { + log.Println("checkCSRFToken FAILED") + errorResponse(w, http.StatusBadRequest, nil, nil) + return + } + + next.ServeHTTP(w, r) + }) +} + +func (a *API) checkCSRFToken(r *http.Request) bool { + token := r.Header.Get(HEADER_REQUESTED_WITH) + + if token == HEADER_REQUESTED_WITH_XML { + return true + } + + return false +} + func (a *API) handleGetBlocks(w http.ResponseWriter, r *http.Request) { query := r.URL.Query() parentID := query.Get("parent_id") diff --git a/server/client/client.go b/server/client/client.go index 82a99d2f9..c6814f3ba 100644 --- a/server/client/client.go +++ b/server/client/client.go @@ -65,7 +65,10 @@ type Client struct { func NewClient(url string) *Client { url = strings.TrimRight(url, "/") - return &Client{url, url + API_URL_SUFFIX, &http.Client{}, map[string]string{}} + headers := map[string]string{ + "X-Requested-With": "XMLHttpRequest", + } + return &Client{url, url + API_URL_SUFFIX, &http.Client{}, headers} } func (c *Client) DoApiGet(url string, etag string) (*http.Response, error) { diff --git a/webapp/src/octoClient.ts b/webapp/src/octoClient.ts index b2ff157f2..8ae7fa538 100644 --- a/webapp/src/octoClient.ts +++ b/webapp/src/octoClient.ts @@ -87,6 +87,7 @@ class OctoClient { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: this.token ? 'Bearer ' + this.token : '', + 'X-Requested-With': 'XMLHttpRequest', } } @@ -231,14 +232,14 @@ class OctoClient { formData.append('file', file) try { + const headers = this.headers() as Record + + // TIPTIP: Leave out Content-Type here, it will be automatically set by the browser + delete headers['Content-Type'] + const response = await fetch(this.serverUrl + '/api/v1/files', { method: 'POST', - - // TIPTIP: Leave out Content-Type here, it will be automatically set by the browser - headers: { - Accept: 'application/json', - Authorization: this.token ? 'Bearer ' + this.token : '', - }, + headers, body: formData, }) if (response.status !== 200) {