You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

187 lines
5.2KB

  1. /* ----------------------------------------------------------------------
  2. * ______ ___ __
  3. * / ____/___ / | ____ __ ___ __/ /_ ___ ________
  4. * / / __/ __ \/ /| | / __ \/ / / / | /| / / __ \/ _ \/ ___/ _ \
  5. * / /_/ / /_/ / ___ |/ / / / /_/ /| |/ |/ / / / / __/ / / __/
  6. * \____/\____/_/ |_/_/ /_/\__. / |__/|__/_/ /_/\___/_/ \___/
  7. * /____/
  8. *
  9. * (C) Copyright 2015 GoAnywhere (http://goanywhere.io).
  10. * ----------------------------------------------------------------------
  11. * Licensed under the Apache License, Version 2.0 (the "License");
  12. * you may not use this file except in compliance with the License.
  13. * You may obtain a copy of the License at
  14. *
  15. * http://www.apache.org/licenses/LICENSE-2.0
  16. *
  17. * Unless required by applicable law or agreed to in writing, software
  18. * distributed under the License is distributed on an "AS IS" BASIS,
  19. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  20. * See the License for the specific language governing permissions and
  21. * limitations under the License.
  22. * ----------------------------------------------------------------------*/
  23. package middleware
  24. import (
  25. "bytes"
  26. "crypto/hmac"
  27. "crypto/sha1"
  28. "crypto/subtle"
  29. "encoding/base64"
  30. "encoding/hex"
  31. "fmt"
  32. "net/http"
  33. "net/url"
  34. "regexp"
  35. "strconv"
  36. "time"
  37. "github.com/goanywhere/x/crypto"
  38. )
  39. const (
  40. xsrfCookieName = "xsrf"
  41. xsrfHeaderName = "X-XSRF-Token"
  42. xsrfFieldName = "xsrftoken"
  43. xsrfMaxAge = 3600 * 24 * 365
  44. xsrfTimeout = time.Hour * 24 * 365
  45. )
  46. var (
  47. errXSRFReferer = "Referer URL is missing from the request or the value was malformed."
  48. errXSRFToken = "Invalid XSRF tokens"
  49. xsrfPattern = regexp.MustCompile("[^0-9a-zA-Z-_]")
  50. unsafeMethods = regexp.MustCompile("^(DELETE|POST|PUT)$")
  51. )
  52. type xsrf struct {
  53. *http.Request
  54. http.ResponseWriter
  55. token string
  56. }
  57. // See http://en.wikipedia.org/wiki/Same-origin_policy
  58. func (self *xsrf) checkOrigin() bool {
  59. if self.Request.URL.Scheme == "https" {
  60. // See [OWASP]; Checking the Referer Header.
  61. referer, err := url.Parse(self.Request.Header.Get("Referer"))
  62. if err != nil || referer.String() == "" ||
  63. referer.Scheme != self.Request.URL.Scheme ||
  64. referer.Host != self.Request.URL.Host {
  65. return false
  66. }
  67. }
  68. return true
  69. }
  70. func (self *xsrf) checkToken(token string) bool {
  71. // Header always takes precedance of form field since some popular
  72. // JavaScript frameworks allow global custom headers for all AJAX requests.
  73. query := self.Request.Header.Get(xsrfFieldName)
  74. if query == "" {
  75. query = self.Request.FormValue(xsrfFieldName)
  76. }
  77. // 1) basic length comparison.
  78. if query == "" || len(query) != len(token) {
  79. return false
  80. }
  81. // *sanitize* incoming masked token.
  82. query = xsrfPattern.ReplaceAllString(query, "")
  83. // 2) byte-based comparison.
  84. a, _ := base64.URLEncoding.DecodeString(token)
  85. b, _ := base64.URLEncoding.DecodeString(query)
  86. if subtle.ConstantTimeCompare(a, b) != 1 {
  87. return false
  88. }
  89. // 3) issued time checking.
  90. index := bytes.LastIndex(b, []byte{'|'})
  91. if index != 40 {
  92. return false
  93. }
  94. nanos, err := strconv.ParseInt(string(b[index+1:]), 10, 64)
  95. if err != nil {
  96. return false
  97. }
  98. now := time.Now()
  99. issueTime := time.Unix(0, nanos)
  100. if now.Sub(issueTime) >= xsrfTimeout {
  101. return false
  102. }
  103. // Ensure the token is not from the *future*, allow 1 minute grace period.
  104. if issueTime.After(now.Add(1 * time.Minute)) {
  105. return false
  106. }
  107. return true
  108. }
  109. func (self *xsrf) generate() {
  110. // Ensure we have XSRF token in the cookie first.
  111. var token string
  112. if cookie, err := self.Request.Cookie(xsrfCookieName); err == nil {
  113. if cookie.Value != "" {
  114. token = cookie.Value
  115. }
  116. }
  117. if token == "" {
  118. // Generate a base64-encoded token.
  119. nano := time.Now().UnixNano()
  120. hash := hmac.New(sha1.New, []byte(crypto.Random(32)))
  121. fmt.Fprintf(hash, "%s|%d", crypto.Random(12), nano)
  122. raw := fmt.Sprintf("%s|%d", hex.EncodeToString(hash.Sum(nil)), nano)
  123. token = base64.URLEncoding.EncodeToString([]byte(raw))
  124. // The max-age directive takes priority over Expires.
  125. // http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html
  126. cookie := new(http.Cookie)
  127. cookie.Name = xsrfCookieName
  128. cookie.Value = token
  129. cookie.MaxAge = xsrfMaxAge
  130. cookie.Path = "/"
  131. cookie.HttpOnly = true
  132. http.SetCookie(self.ResponseWriter, cookie)
  133. }
  134. self.ResponseWriter.Header()[xsrfHeaderName] = []string{token}
  135. self.token = token
  136. }
  137. // ---------------------------------------------------------------------------
  138. // XSRF Middleware Supports
  139. // ---------------------------------------------------------------------------
  140. func XSRF(next http.Handler) http.Handler {
  141. fn := func(w http.ResponseWriter, r *http.Request) {
  142. x := new(xsrf)
  143. x.Request = r
  144. x.ResponseWriter = w
  145. x.generate()
  146. if unsafeMethods.MatchString(r.Method) {
  147. // Ensure the URL came for "Referer" under HTTPS.
  148. if !x.checkOrigin() {
  149. http.Error(w, errXSRFReferer, http.StatusForbidden)
  150. }
  151. // length => bytes => issue time checkpoints.
  152. if !x.checkToken(x.token) {
  153. http.Error(w, errXSRFToken, http.StatusForbidden)
  154. }
  155. }
  156. // ensure browser will invalidate the cached XSRF token.
  157. w.Header().Add("Vary", "Cookie")
  158. next.ServeHTTP(w, r)
  159. }
  160. return http.HandlerFunc(fn)
  161. }