diff --git a/internal/driver/webui.go b/internal/driver/webui.go index a747635c59..a2fcda6739 100644 --- a/internal/driver/webui.go +++ b/internal/driver/webui.go @@ -132,8 +132,8 @@ func serveWebInterface(hostport string, p *profile.Profile, o *plugin.Options, d "/source": http.HandlerFunc(ui.source), "/peek": http.HandlerFunc(ui.peek), "/flamegraph": http.HandlerFunc(ui.stackView), - "/saveconfig": http.HandlerFunc(ui.saveConfig), - "/deleteconfig": http.HandlerFunc(ui.deleteConfig), + "/saveconfig": configHandler(http.MethodPost, ui.saveConfig), + "/deleteconfig": configHandler(http.MethodDelete, ui.deleteConfig), "/download": http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Content-Type", "application/vnd.google.protobuf+gzip") w.Header().Set("Content-Disposition", "attachment;filename=profile.pb.gz") @@ -449,6 +449,21 @@ func (ui *webInterface) peek(w http.ResponseWriter, req *http.Request) { }) } +// configHandler wraps a handler that modifies the settings file. It only +// accepts the given method and rejects cross-origin browser requests, so +// that other web sites open in the user's browser cannot change or delete +// saved configurations. +func configHandler(method string, h http.HandlerFunc) http.Handler { + return http.NewCrossOriginProtection().Handler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + if req.Method != method { + w.Header().Set("Allow", method) + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + return + } + h(w, req) + })) +} + // saveConfig saves URL configuration. func (ui *webInterface) saveConfig(w http.ResponseWriter, req *http.Request) { if err := setConfig(ui.settingsFile, *req.URL); err != nil { diff --git a/internal/driver/webui_test.go b/internal/driver/webui_test.go index 59622eac04..b1e3be01f7 100644 --- a/internal/driver/webui_test.go +++ b/internal/driver/webui_test.go @@ -155,6 +155,66 @@ func TestWebInterface(t *testing.T) { wg.Wait() } +func TestConfigHandlersRejectCrossOrigin(t *testing.T) { + // Keep the settings file out of the user's config directory. + dir := t.TempDir() + for _, env := range []string{"HOME", "home", "XDG_CONFIG_HOME", "AppData"} { + t.Setenv(env, dir) + } + fname, err := settingsFileName() + if err != nil { + t.Fatal(err) + } + server := makeTestServer(t, makeFakeProfile()) + + hasConfig := func(name string) bool { + s, err := readSettings(fname) + if err != nil { + t.Fatal(err) + } + for _, c := range s.Configs { + if c.Name == name { + return true + } + } + return false + } + + for _, c := range []struct { + desc, method, path string + header map[string]string + wantCode int + wantConfig bool + }{ + {"get save", http.MethodGet, "/saveconfig?config=c1", nil, http.StatusMethodNotAllowed, false}, + {"cross-site save", http.MethodPost, "/saveconfig?config=c1", map[string]string{"Sec-Fetch-Site": "cross-site"}, http.StatusForbidden, false}, + {"cross-origin save", http.MethodPost, "/saveconfig?config=c1", map[string]string{"Origin": "http://example.com"}, http.StatusForbidden, false}, + {"same-origin save", http.MethodPost, "/saveconfig?config=c1", map[string]string{"Sec-Fetch-Site": "same-origin"}, http.StatusOK, true}, + {"get delete", http.MethodGet, "/deleteconfig?config=c1", nil, http.StatusMethodNotAllowed, true}, + {"cross-site delete", http.MethodDelete, "/deleteconfig?config=c1", map[string]string{"Sec-Fetch-Site": "cross-site"}, http.StatusForbidden, true}, + {"same-origin delete", http.MethodDelete, "/deleteconfig?config=c1", map[string]string{"Sec-Fetch-Site": "same-origin"}, http.StatusOK, false}, + } { + req, err := http.NewRequest(c.method, server.URL+c.path, nil) + if err != nil { + t.Fatal(err) + } + for k, v := range c.header { + req.Header.Set(k, v) + } + res, err := http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + res.Body.Close() + if res.StatusCode != c.wantCode { + t.Errorf("%s: got status %d, want %d", c.desc, res.StatusCode, c.wantCode) + } + if got := hasConfig("c1"); got != c.wantConfig { + t.Errorf("%s: config saved = %v, want %v", c.desc, got, c.wantConfig) + } + } +} + // Implement fake object file support. const addrBase = 0x1000