Browse Source

Implement CSRF protection for REST interface (fixes #287)

Jakob Borg 11 years ago
parent
commit
80c2b32b92
4 changed files with 119 additions and 0 deletions
  1. 0 0
      auto/gui.files.go
  2. 3 0
      cmd/syncthing/gui.go
  3. 111 0
      cmd/syncthing/gui_csrf.go
  4. 5 0
      gui/app.js

File diff suppressed because it is too large
+ 0 - 0
auto/gui.files.go


+ 3 - 0
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

+ 111 - 0
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())
+	}
+}

+ 5 - 0
gui/app.js

@@ -10,6 +10,11 @@
 var syncthing = angular.module('syncthing', []);
 var urlbase = 'rest';
 
+syncthing.config(function ($httpProvider) {
+    $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
+    $httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
+});
+
 syncthing.controller('SyncthingCtrl', function ($scope, $http) {
     var prevDate = 0;
     var getOK = true;

Some files were not shown because too many files changed in this diff