summaryrefslogtreecommitdiffstats
path: root/cmd
diff options
context:
space:
mode:
authorJakob Borg <jakob@nym.se>2014-06-04 21:20:07 +0200
committerJakob Borg <jakob@nym.se>2014-06-04 21:20:07 +0200
commit80c2b32b92be3d9dff93e40ecba29aa47411b9b8 (patch)
tree79a6add352fe4d2a896bc68f20ab0d2fa5295ac0 /cmd
parent028e9bc17acc667ee0c1866e1ae041ac40a23b9e (diff)
Implement CSRF protection for REST interface (fixes #287)
Diffstat (limited to 'cmd')
-rw-r--r--cmd/syncthing/gui.go3
-rw-r--r--cmd/syncthing/gui_csrf.go111
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())
+ }
+}