|
| 1 | +// Synchronizer Token Pattern implementation. |
| 2 | +// |
| 3 | +// See [OWASP] https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)_Prevention_Cheat_Sheet |
| 4 | +package csrf |
| 5 | + |
| 6 | +import ( |
| 7 | + "crypto/subtle" |
| 8 | + "github.com/robfig/revel" |
| 9 | + "net/url" |
| 10 | + "regexp" |
| 11 | +) |
| 12 | + |
| 13 | +const ( |
| 14 | + cookieName = "csrf_token" |
| 15 | + fieldName = "csrf_token" |
| 16 | + headerName = "X-CSRF-Token" |
| 17 | +) |
| 18 | + |
| 19 | +var ( |
| 20 | + errNoReferer = "A secure request contained no Referer or its value was malformed." |
| 21 | + errBadReferer = "Same-origin policy failure." |
| 22 | + errBadToken = "CSRF tokens mismatch." |
| 23 | +) |
| 24 | + |
| 25 | +var CSRFFilter = func(c *revel.Controller, fc []revel.Filter) { |
| 26 | + r := c.Request.Request |
| 27 | + |
| 28 | + // OWASP; General Recommendation: Synchronizer Token Pattern. |
| 29 | + // CSRF tokens must be associated with the user's current session. |
| 30 | + tokenCookie, found := c.Session[cookieName] |
| 31 | + realToken := "" |
| 32 | + if !found { |
| 33 | + realToken = generateNewToken(c) |
| 34 | + } else { |
| 35 | + realToken = tokenCookie |
| 36 | + revel.TRACE.Printf("Session's CSRF token: '%s'", realToken) |
| 37 | + if len(realToken) != tokenLength { |
| 38 | + // Wrong length; token has either been tampered with, we're migrating |
| 39 | + // onto a new algorithm for generating tokens, or a new session has |
| 40 | + // been initiated. In any case, a new token is generated and the |
| 41 | + // error will be detected later. |
| 42 | + revel.TRACE.Printf("Bad CSRF token length: found %d, expected %d", |
| 43 | + len(realToken), tokenLength) |
| 44 | + realToken = generateNewToken(c) |
| 45 | + } |
| 46 | + } |
| 47 | + |
| 48 | + c.RenderArgs[fieldName] = realToken |
| 49 | + |
| 50 | + // See http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods |
| 51 | + safeMethod, _ := regexp.MatchString("^(GET|HEAD|OPTIONS|TRACE)$", r.Method) |
| 52 | + if !safeMethod { |
| 53 | + revel.TRACE.Printf("Unsafe %s method...", r.Method) |
| 54 | + if r.URL.Scheme == "https" { |
| 55 | + // See OWASP; Checking the Referer Header. |
| 56 | + referer, err := url.Parse(r.Header.Get("Referer")) |
| 57 | + if err != nil || referer.String() == "" { |
| 58 | + // Parse error or empty referer. |
| 59 | + c.Result = c.Forbidden(errNoReferer) |
| 60 | + return |
| 61 | + } |
| 62 | + // See OWASP; Checking the Origin Header. |
| 63 | + if !sameOrigin(referer, r.URL) { |
| 64 | + c.Result = c.Forbidden(errBadReferer) |
| 65 | + return |
| 66 | + } |
| 67 | + } |
| 68 | + |
| 69 | + // Accept CSRF token in the custom HTTP header X-CSRF-Token, as well as |
| 70 | + // in the form submission itself, for ease of use with popular JavaScript |
| 71 | + // toolkits which allow insertion of custom headers into all AJAX |
| 72 | + // requests. See http://erlend.oftedal.no/blog/?blogid=118 |
| 73 | + sentToken := r.Header.Get(headerName) |
| 74 | + if sentToken == "" { |
| 75 | + sentToken = r.PostFormValue(fieldName) |
| 76 | + } |
| 77 | + revel.TRACE.Printf("CSRF token received: '%s'", sentToken) |
| 78 | + |
| 79 | + if len(sentToken) != len(realToken) { |
| 80 | + c.Result = c.Forbidden(errBadToken) |
| 81 | + return |
| 82 | + } else { |
| 83 | + comparison := subtle.ConstantTimeCompare([]byte(sentToken), []byte(realToken)) |
| 84 | + if comparison != 1 { |
| 85 | + c.Result = c.Forbidden(errBadToken) |
| 86 | + return |
| 87 | + } |
| 88 | + } |
| 89 | + } |
| 90 | + revel.TRACE.Println("CSRF token successfully checked.") |
| 91 | + |
| 92 | + fc[0](c, fc[1:]) |
| 93 | +} |
| 94 | + |
| 95 | +// See http://en.wikipedia.org/wiki/Same-origin_policy |
| 96 | +func sameOrigin(u1, u2 *url.URL) bool { |
| 97 | + return (u1.Scheme == u2.Scheme && u1.Host == u2.Host) |
| 98 | +} |
| 99 | + |
| 100 | +// Generate a new CSRF token. |
| 101 | +func generateNewToken(c *revel.Controller) string { |
| 102 | + token := generateToken() |
| 103 | + revel.TRACE.Printf("Generated new CSRF Token: '%s'", token) |
| 104 | + c.Session[cookieName] = token |
| 105 | + return token |
| 106 | +} |
0 commit comments