diff options
author | Jakob Borg <jakob@nym.se> | 2014-06-04 21:20:07 +0200 |
---|---|---|
committer | Jakob Borg <jakob@nym.se> | 2014-06-04 21:20:07 +0200 |
commit | 80c2b32b92be3d9dff93e40ecba29aa47411b9b8 (patch) | |
tree | 79a6add352fe4d2a896bc68f20ab0d2fa5295ac0 /cmd | |
parent | 028e9bc17acc667ee0c1866e1ae041ac40a23b9e (diff) |
Implement CSRF protection for REST interface (fixes #287)
Diffstat (limited to 'cmd')
-rw-r--r-- | cmd/syncthing/gui.go | 3 | ||||
-rw-r--r-- | cmd/syncthing/gui_csrf.go | 111 |
2 files changed, 114 insertions, 0 deletions
diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index e6d8b21faa..d98e16a98f 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -105,6 +105,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro router.Post("/rest/discovery/hint", restPostDiscoveryHint) mr := martini.New() + mr.Use(csrfMiddleware) if len(cfg.User) > 0 && len(cfg.Password) > 0 { mr.Use(basic(cfg.User, cfg.Password)) } @@ -114,6 +115,8 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro mr.Action(router.Handle) mr.Map(m) + loadCsrfTokens() + go http.Serve(listener, mr) return nil diff --git a/cmd/syncthing/gui_csrf.go b/cmd/syncthing/gui_csrf.go new file mode 100644 index 0000000000..f7a39f5a1c --- /dev/null +++ b/cmd/syncthing/gui_csrf.go @@ -0,0 +1,111 @@ +package main + +import ( + "bufio" + "crypto/rand" + "encoding/base64" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + "sync" + "time" + + "github.com/calmh/syncthing/osutil" +) + +var csrfTokens []string +var csrfMut sync.Mutex + +// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject +// the request with 403. For / and /index.html, set a new CSRF cookie if none +// is currently set. +func csrfMiddleware(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, "/rest/") { + token := r.Header.Get("X-CSRF-Token") + if !validCsrfToken(token) { + http.Error(w, "CSRF Error", 403) + } + } else if r.URL.Path == "/" || r.URL.Path == "/index.html" { + cookie, err := r.Cookie("CSRF-Token") + if err != nil || !validCsrfToken(cookie.Value) { + cookie = &http.Cookie{ + Name: "CSRF-Token", + Value: newCsrfToken(), + } + http.SetCookie(w, cookie) + } + } +} + +func validCsrfToken(token string) bool { + csrfMut.Lock() + defer csrfMut.Unlock() + for _, t := range csrfTokens { + if t == token { + return true + } + } + return false +} + +func newCsrfToken() string { + bs := make([]byte, 30) + _, err := rand.Reader.Read(bs) + if err != nil { + l.Fatalln(err) + } + + token := base64.StdEncoding.EncodeToString(bs) + + csrfMut.Lock() + csrfTokens = append(csrfTokens, token) + if len(csrfTokens) > 10 { + csrfTokens = csrfTokens[len(csrfTokens)-10:] + } + defer csrfMut.Unlock() + + saveCsrfTokens() + + return token +} + +func saveCsrfTokens() { + name := filepath.Join(confDir, "csrftokens.txt") + tmp := fmt.Sprintf("%s.tmp.%d", name, time.Now().UnixNano()) + + f, err := os.OpenFile(tmp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) + if err != nil { + return + } + defer os.Remove(tmp) + + for _, t := range csrfTokens { + _, err := fmt.Fprintln(f, t) + if err != nil { + return + } + } + + err = f.Close() + if err != nil { + return + } + + osutil.Rename(tmp, name) +} + +func loadCsrfTokens() { + name := filepath.Join(confDir, "csrftokens.txt") + f, err := os.Open(name) + if err != nil { + return + } + defer f.Close() + + s := bufio.NewScanner(f) + for s.Scan() { + csrfTokens = append(csrfTokens, s.Text()) + } +} |