api_csrf.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. // Copyright (C) 2014 The Syncthing Authors.
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this file,
  5. // You can obtain one at https://mozilla.org/MPL/2.0/.
  6. package api
  7. import (
  8. "bufio"
  9. "fmt"
  10. "net/http"
  11. "os"
  12. "strings"
  13. "github.com/syncthing/syncthing/lib/config"
  14. "github.com/syncthing/syncthing/lib/locations"
  15. "github.com/syncthing/syncthing/lib/osutil"
  16. "github.com/syncthing/syncthing/lib/rand"
  17. "github.com/syncthing/syncthing/lib/sync"
  18. )
  19. // csrfTokens is a list of valid tokens. It is sorted so that the most
  20. // recently used token is first in the list. New tokens are added to the front
  21. // of the list (as it is the most recently used at that time). The list is
  22. // pruned to a maximum of maxCsrfTokens, throwing away the least recently used
  23. // tokens.
  24. var csrfTokens []string
  25. var csrfMut = sync.NewMutex()
  26. const maxCsrfTokens = 25
  27. // Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
  28. // the request with 403. For / and /index.html, set a new CSRF cookie if none
  29. // is currently set.
  30. func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, next http.Handler) http.Handler {
  31. loadCsrfTokens()
  32. return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  33. // Allow requests carrying a valid API key
  34. if cfg.IsValidAPIKey(r.Header.Get("X-API-Key")) {
  35. // Set the access-control-allow-origin header for CORS requests
  36. // since a valid API key has been provided
  37. w.Header().Add("Access-Control-Allow-Origin", "*")
  38. next.ServeHTTP(w, r)
  39. return
  40. }
  41. if strings.HasPrefix(r.URL.Path, "/rest/debug") {
  42. // Debugging functions are only available when explicitly
  43. // enabled, and can be accessed without a CSRF token
  44. next.ServeHTTP(w, r)
  45. return
  46. }
  47. // Allow requests for anything not under the protected path prefix,
  48. // and set a CSRF cookie if there isn't already a valid one.
  49. if !strings.HasPrefix(r.URL.Path, prefix) {
  50. cookie, err := r.Cookie("CSRF-Token-" + unique)
  51. if err != nil || !validCsrfToken(cookie.Value) {
  52. l.Debugln("new CSRF cookie in response to request for", r.URL)
  53. cookie = &http.Cookie{
  54. Name: "CSRF-Token-" + unique,
  55. Value: newCsrfToken(),
  56. }
  57. http.SetCookie(w, cookie)
  58. }
  59. next.ServeHTTP(w, r)
  60. return
  61. }
  62. // Verify the CSRF token
  63. token := r.Header.Get("X-CSRF-Token-" + unique)
  64. if !validCsrfToken(token) {
  65. http.Error(w, "CSRF Error", 403)
  66. return
  67. }
  68. next.ServeHTTP(w, r)
  69. })
  70. }
  71. func validCsrfToken(token string) bool {
  72. csrfMut.Lock()
  73. defer csrfMut.Unlock()
  74. for i, t := range csrfTokens {
  75. if t == token {
  76. if i > 0 {
  77. // Move this token to the head of the list. Copy the tokens at
  78. // the front one step to the right and then replace the token
  79. // at the head.
  80. copy(csrfTokens[1:], csrfTokens[:i+1])
  81. csrfTokens[0] = token
  82. }
  83. return true
  84. }
  85. }
  86. return false
  87. }
  88. func newCsrfToken() string {
  89. token := rand.String(32)
  90. csrfMut.Lock()
  91. csrfTokens = append([]string{token}, csrfTokens...)
  92. if len(csrfTokens) > maxCsrfTokens {
  93. csrfTokens = csrfTokens[:maxCsrfTokens]
  94. }
  95. defer csrfMut.Unlock()
  96. saveCsrfTokens()
  97. return token
  98. }
  99. func saveCsrfTokens() {
  100. // We're ignoring errors in here. It's not super critical and there's
  101. // nothing relevant we can do about them anyway...
  102. name := locations.Get(locations.CsrfTokens)
  103. f, err := osutil.CreateAtomic(name)
  104. if err != nil {
  105. return
  106. }
  107. for _, t := range csrfTokens {
  108. fmt.Fprintln(f, t)
  109. }
  110. f.Close()
  111. }
  112. func loadCsrfTokens() {
  113. f, err := os.Open(locations.Get(locations.CsrfTokens))
  114. if err != nil {
  115. return
  116. }
  117. defer f.Close()
  118. s := bufio.NewScanner(f)
  119. for s.Scan() {
  120. csrfTokens = append(csrfTokens, s.Text())
  121. }
  122. }