From 72d673460a4f08653bc9fb52a2c957d09dd578b4 Mon Sep 17 00:00:00 2001 From: ubeddulla khan Date: Fri, 16 Oct 2026 17:24:49 +0530 Subject: [PATCH] internal/driver: reject cross-origin requests to saveconfig and deleteconfig The handlers accepted any method and had no origin check, so any web page open in the user's browser could add, overwrite or delete saved configurations while the web UI was running. Only accept the method the UI uses and apply http.CrossOriginProtection. --- internal/driver/webui.go | 19 +++++++++-- internal/driver/webui_test.go | 60 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 2 deletions(-) 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