126 lines
3.3 KiB
Go
126 lines
3.3 KiB
Go
package csrf
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/subtle"
|
|
"encoding/hex"
|
|
"html/template"
|
|
"io"
|
|
"math"
|
|
"net/url"
|
|
|
|
"github.com/revel/revel"
|
|
)
|
|
|
|
// allowMethods are HTTP methods that do NOT require a token
|
|
var allowedMethods = map[string]bool{
|
|
"GET": true,
|
|
"HEAD": true,
|
|
"OPTIONS": true,
|
|
"TRACE": true,
|
|
}
|
|
|
|
func RandomString(length int) (string, error) {
|
|
buffer := make([]byte, int(math.Ceil(float64(length)/2)))
|
|
if _, err := io.ReadFull(rand.Reader, buffer); err != nil {
|
|
return "", nil
|
|
}
|
|
str := hex.EncodeToString(buffer)
|
|
return str[:length], nil
|
|
}
|
|
|
|
func RefreshToken(c *revel.Controller) {
|
|
token, err := RandomString(64)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
c.Session["csrf_token"] = token
|
|
}
|
|
|
|
// CsrfFilter enables CSRF request token creation and verification.
|
|
//
|
|
// Usage:
|
|
// 1) Add `csrf.CsrfFilter` to the app's filters (it must come after the revel.SessionFilter).
|
|
// 2) Add CSRF fields to a form with the template tag `{{ csrftoken . }}`. The filter adds a function closure to the `ViewArgs` that can pull out the secret and make the token as-needed, caching the value in the request. Ajax support provided through the `X-CSRFToken` header.
|
|
func CsrfFilter(c *revel.Controller, fc []revel.Filter) {
|
|
token, foundToken := c.Session["csrf_token"]
|
|
|
|
if !foundToken {
|
|
RefreshToken(c)
|
|
}
|
|
|
|
referer, refErr := url.Parse(c.Request.Referer())
|
|
if refErr != nil {
|
|
c.Result = c.Forbidden("REVEL CSRF: Unable to fetch referer")
|
|
return
|
|
}
|
|
isSameOrigin := sameOrigin(c.Request.URL, referer)
|
|
|
|
// If the Request method isn't in the white listed methods
|
|
if !allowedMethods[c.Request.Method] && !IsExempt(c) {
|
|
// Token wasn't present at all
|
|
if !foundToken {
|
|
c.Result = c.Forbidden("REVEL CSRF: Session token missing.")
|
|
return
|
|
}
|
|
|
|
// Referer header is invalid
|
|
if refErr != nil {
|
|
c.Result = c.Forbidden("REVEL CSRF: HTTP Referer malformed.")
|
|
return
|
|
}
|
|
|
|
// Same origin
|
|
if !isSameOrigin {
|
|
c.Result = c.Forbidden("REVEL CSRF: Same origin mismatch.")
|
|
return
|
|
}
|
|
|
|
var requestToken string
|
|
// First check for token in post data
|
|
if c.Request.Method == "POST" {
|
|
requestToken = c.Params.Get("csrftoken")
|
|
}
|
|
|
|
// Then check for token in custom headers, as with AJAX
|
|
if requestToken == "" {
|
|
requestToken = c.Request.GetHttpHeader("X-CSRFToken")
|
|
}
|
|
|
|
if requestToken == "" || !compareToken(requestToken, token) {
|
|
c.Result = c.Forbidden("REVEL CSRF: Invalid token.")
|
|
return
|
|
}
|
|
}
|
|
|
|
fc[0](c, fc[1:])
|
|
|
|
// Only add token to ViewArgs if the request is: not AJAX, not missing referer header, and is same origin.
|
|
if c.Request.GetHttpHeader("X-CSRFToken") == "" && isSameOrigin {
|
|
c.ViewArgs["_csrftoken"] = token
|
|
}
|
|
}
|
|
|
|
func compareToken(requestToken, token string) bool {
|
|
// ConstantTimeCompare will panic if the []byte aren't the same length
|
|
if len(requestToken) != len(token) {
|
|
return false
|
|
}
|
|
return subtle.ConstantTimeCompare([]byte(requestToken), []byte(token)) == 1
|
|
}
|
|
|
|
// Validates same origin policy
|
|
func sameOrigin(u1, u2 *url.URL) bool {
|
|
return u1.Scheme == u2.Scheme && u1.Host == u2.Host
|
|
}
|
|
|
|
func init() {
|
|
revel.TemplateFuncs["csrftoken"] = func(viewArgs map[string]interface{}) template.HTML {
|
|
if tokenFunc, ok := viewArgs["_csrftoken"]; !ok {
|
|
panic("REVEL CSRF: _csrftoken missing from ViewArgs.")
|
|
} else {
|
|
return template.HTML(tokenFunc.(string))
|
|
}
|
|
}
|
|
}
|