Skip to content

Commit 0611ca9

Browse files
committed
Added configuration options and route exemptions
1 parent 0875114 commit 0611ca9

File tree

10 files changed

+280
-135
lines changed

10 files changed

+280
-135
lines changed

README.md

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,23 @@ prevention for the [Revel framework](https://github.com/robfig/revel).
77
Code is based on the `nosurf` package implemented by
88
[Justinas Stankevičius](https://github.com/justinas/nosurf).
99

10-
## Limitations
11-
12-
Package does not yet include provision to exempt specific routes from
13-
CSRF checks.
14-
1510
## Installation
1611

1712
go get github.com/cbonello/revel-csrf
1813

14+
## Configuration options
15+
16+
Revel-csrf supports following configuration options in `app.conf`:
17+
18+
* `csrf.ajax`
19+
A boolean value that indicates whether or not `revel-csrf` should support the injection and verification of CSRF tokens for XMLHttpRequests. Default value is `false`.
20+
21+
* `csrf.token.length`
22+
An integer value that defines the number of characters that should be found within CSRF tokens. Token length should be in [32..512] and default value is 32 characters.
23+
1924
## Operating instructions
2025

21-
Simply call the CSRFFilter() filter from init.go:
26+
Simply call the CSRFFilter() filter in `app/init.go` right after `revel.SessionFilter`. The CSRF token is saved in the session cookie.
2227

2328
package app
2429

@@ -35,8 +40,8 @@ Simply call the CSRFFilter() filter from init.go:
3540
revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
3641
revel.ParamsFilter, // Parse parameters into Controller.Params.
3742
revel.SessionFilter, // Restore and write the session cookie.
38-
revel.FlashFilter, // Restore and write the flash cookie.
3943
csrf.CSRFFilter, // CSRF prevention.
44+
revel.FlashFilter, // Restore and write the flash cookie.
4045
revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie.
4146
revel.I18nFilter, // Resolve the requested language
4247
revel.InterceptorFilter, // Run interceptors around the action.
@@ -91,8 +96,9 @@ A demo application is provided in the samples directory. To launch it:
9196

9297
## TODO
9398

94-
* Routes exemption.
95-
* Logger.
9699
* Unique token per-page.
97-
* Configuration options.
98100
* Test cases.
101+
102+
## CONTRIBUTORS
103+
* Otto Bretz
104+
* Allen Dang

csrf.go

Lines changed: 77 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -4,104 +4,101 @@
44
package csrf
55

66
import (
7-
"crypto/subtle"
8-
"github.com/golang/glog"
9-
"github.com/robfig/revel"
10-
"net/url"
11-
"regexp"
7+
"crypto/subtle"
8+
"github.com/golang/glog"
9+
"github.com/robfig/revel"
10+
"net/url"
11+
"regexp"
1212
)
1313

1414
const (
15-
cookieName = "csrf_token"
16-
fieldName = "csrf_token"
17-
headerName = "X-CSRF-Token"
15+
cookieName = "csrf_token"
16+
fieldName = "csrf_token"
17+
headerName = "X-CSRF-Token"
1818
)
1919

2020
var (
21-
errNoReferer = "A secure request contained no Referer or its value was malformed."
22-
errBadReferer = "Same-origin policy failure."
23-
errBadToken = "CSRF tokens mismatch."
21+
errNoReferer = "REVEL_CSRF: A secure request contained no Referer or its value was malformed."
22+
errBadReferer = "REVEL_CSRF: Same-origin policy failure."
23+
errBadToken = "REVEL_CSRF: tokens mismatch."
24+
safeMethods = regexp.MustCompile("^(GET|HEAD|OPTIONS|TRACE)$")
2425
)
2526

2627
var CSRFFilter = func(c *revel.Controller, fc []revel.Filter) {
27-
r := c.Request.Request
28+
r := c.Request.Request
2829

29-
// OWASP; General Recommendation: Synchronizer Token Pattern.
30-
// CSRF tokens must be associated with the user's current session.
31-
tokenCookie, found := c.Session[cookieName]
32-
realToken := ""
33-
if !found {
34-
realToken = generateNewToken(c)
35-
} else {
36-
realToken = tokenCookie
37-
glog.V(0).Infof("Session's CSRF token: '%s'", realToken)
38-
if len(realToken) != tokenLength {
39-
// Wrong length; token has either been tampered with, we're migrating
40-
// onto a new algorithm for generating tokens, or a new session has
41-
// been initiated. In any case, a new token is generated and the
42-
// error will be detected later.
43-
glog.V(0).Infof("Bad CSRF token length: found %d, expected %d",
44-
len(realToken), tokenLength)
45-
realToken = generateNewToken(c)
46-
}
47-
}
30+
// [OWASP]; General Recommendation: Synchronizer Token Pattern:
31+
// CSRF tokens must be associated with the user's current session.
32+
tokenCookie, found := c.Session[cookieName]
33+
realToken := ""
34+
if !found {
35+
realToken = generateNewToken(c)
36+
} else {
37+
realToken = tokenCookie
38+
glog.V(2).Infof("REVEL-CSRF: Session's token: '%s'", realToken)
39+
if len(realToken) != lengthCSRFToken {
40+
// Wrong length; token has either been tampered with, we're migrating
41+
// onto a new algorithm for generating tokens, or a new session has
42+
// been initiated. In any case, a new token is generated and the
43+
// error will be detected later.
44+
glog.V(2).Infof("REVEL_CSRF: Bad token length: found %d, expected %d",
45+
len(realToken), lengthCSRFToken)
46+
realToken = generateNewToken(c)
47+
}
48+
}
4849

49-
c.RenderArgs[fieldName] = realToken
50+
c.RenderArgs[fieldName] = realToken
5051

51-
// See http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods
52-
safeMethod, _ := regexp.MatchString("^(GET|HEAD|OPTIONS|TRACE)$", r.Method)
53-
if !safeMethod {
54-
glog.V(0).Infof("Unsafe %s method...", r.Method)
55-
if r.URL.Scheme == "https" {
56-
// See OWASP; Checking the Referer Header.
57-
referer, err := url.Parse(r.Header.Get("Referer"))
58-
if err != nil || referer.String() == "" {
59-
// Parse error or empty referer.
60-
c.Result = c.Forbidden(errNoReferer)
61-
return
62-
}
63-
// See OWASP; Checking the Origin Header.
64-
if !sameOrigin(referer, r.URL) {
65-
c.Result = c.Forbidden(errBadReferer)
66-
return
67-
}
68-
}
52+
// See http://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#Safe_methods
53+
unsafeMethod := !safeMethods.MatchString(r.Method)
54+
if unsafeMethod && !IsExempted(r.URL.Path) {
55+
glog.V(2).Infof("REVEL-CSRF: Processing unsafe '%s' method...", r.Method)
56+
if r.URL.Scheme == "https" {
57+
// See [OWASP]; Checking the Referer Header.
58+
referer, err := url.Parse(r.Header.Get("Referer"))
59+
if err != nil || referer.String() == "" {
60+
// Parse error or empty referer.
61+
c.Result = c.Forbidden(errNoReferer)
62+
return
63+
}
64+
// See [OWASP]; Checking the Origin Header.
65+
if !sameOrigin(referer, r.URL) {
66+
c.Result = c.Forbidden(errBadReferer)
67+
return
68+
}
69+
}
6970

70-
// Accept CSRF token in the custom HTTP header X-CSRF-Token, as well as
71-
// in the form submission itself, for ease of use with popular JavaScript
72-
// toolkits which allow insertion of custom headers into all AJAX
73-
// requests. See http://erlend.oftedal.no/blog/?blogid=118
74-
sentToken := r.Header.Get(headerName)
75-
if sentToken == "" {
76-
sentToken = c.Params.Get(fieldName)
77-
}
78-
glog.V(0).Infof("CSRF token received: '%s'", sentToken)
71+
sentToken := ""
72+
if ajaxSupport := revel.Config.BoolDefault("csrf.ajax", false); ajaxSupport {
73+
// Accept CSRF token in the custom HTTP header X-CSRF-Token, for ease
74+
// of use with popular JavaScript toolkits which allow insertion of
75+
// custom headers into all AJAX requests.
76+
// See http://erlend.oftedal.no/blog/?blogid=118
77+
sentToken = r.Header.Get(headerName)
78+
}
79+
if sentToken == "" {
80+
// Get CSRF token from form.
81+
sentToken = c.Params.Get(fieldName)
82+
}
83+
glog.V(2).Infof("REVEL-CSRF: Token received from client: '%s'", sentToken)
7984

80-
if len(sentToken) != len(realToken) {
81-
c.Result = c.Forbidden(errBadToken)
82-
return
83-
} else {
84-
comparison := subtle.ConstantTimeCompare([]byte(sentToken), []byte(realToken))
85-
if comparison != 1 {
86-
c.Result = c.Forbidden(errBadToken)
87-
return
88-
}
89-
}
90-
}
91-
glog.V(0).Infoln("CSRF token successfully checked.")
85+
if len(sentToken) != len(realToken) {
86+
c.Result = c.Forbidden(errBadToken)
87+
return
88+
}
89+
comparison := subtle.ConstantTimeCompare([]byte(sentToken), []byte(realToken))
90+
if comparison != 1 {
91+
c.Result = c.Forbidden(errBadToken)
92+
return
93+
}
94+
glog.V(2).Infoln("REVEL-CSRF: Token successfully checked.")
95+
}
9296

93-
fc[0](c, fc[1:])
97+
fc[0](c, fc[1:])
9498
}
9599

96100
// See http://en.wikipedia.org/wiki/Same-origin_policy
97101
func sameOrigin(u1, u2 *url.URL) bool {
98-
return (u1.Scheme == u2.Scheme && u1.Host == u2.Host)
102+
return (u1.Scheme == u2.Scheme && u1.Host == u2.Host)
99103
}
100104

101-
// Generate a new CSRF token.
102-
func generateNewToken(c *revel.Controller) string {
103-
token := generateToken()
104-
glog.V(0).Infof("Generated new CSRF Token: '%s'", token)
105-
c.Session[cookieName] = token
106-
return token
107-
}

samples/demo/app/controllers/app.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ func (c App) Index() revel.Result {
1515
}
1616

1717
func (c App) Hello(name string) revel.Result {
18+
route := c.Request.Request.URL.Path
19+
exempted := (route == "/HelloExempted")
20+
return c.Render(name, exempted)
21+
}
22+
23+
func (c App) Hello123Exempted(name string) revel.Result {
1824
return c.Render(name)
1925
}
2026

samples/demo/app/init.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ func init() {
1313
revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
1414
revel.ParamsFilter, // Parse parameters into Controller.Params.
1515
revel.SessionFilter, // Restore and write the session cookie.
16-
revel.FlashFilter, // Restore and write the flash cookie.
1716
csrf.CSRFFilter, // CSRF prevention.
17+
revel.FlashFilter, // Restore and write the flash cookie.
1818
revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie.
1919
revel.I18nFilter, // Resolve the requested language
2020
revel.InterceptorFilter, // Run interceptors around the action.
2121
revel.ActionInvoker, // Invoke the action.
2222
}
23+
24+
csrf.ExemptedFullPath("/HelloExempted")
25+
csrf.ExemptedGlob("/Hello[0-9]?3/Exempted")
2326
}

samples/demo/app/views/App/Hello.html

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,38 @@ <h1>{{if .name}}Hello {{ .name }}{{else}}Hello anonymous user{{end}}</h1>
1818
{{template "flash.html" .}}
1919
</div>
2020
</div>
21-
</div>
22-
23-
<div class="container">
21+
{{if .exempted}}
22+
<div class="row">
23+
<h2>This route is exempted from CSRF checks.</h2>
24+
<p>
25+
See <b>github.com/cbonello/revel-csrf/samples/demo/app/init.go</b>: csrf.ExemptedFullPath("/HelloExempted")
26+
</p>
27+
</div>
28+
<div class="row">
29+
<p>
30+
Clicking on the Go Back link will trigger generation of a new index
31+
page and reset the CSRF token to a valid value. Click on your browser's
32+
Back button if you want to keep the altered CSRF token. This behaviour
33+
is caused by the demo implementation and does not exhibits any bug in
34+
revel-csrf.
35+
</p>
36+
<p>
37+
<b>Note:</b> Clicking on Go Back is your best option if you reached
38+
this page through an AJAX call. But that will reset the CSRF token to
39+
a valid value!
40+
</p>
41+
</div>
42+
{{else}}
43+
<div class="row">
44+
<h2>This route is not exempted from CSRF checks.</h2>
45+
</div>
46+
<div class="row">
47+
<p>
48+
<b>Note:</b> Clicking on Go Back is your best option if you reached
49+
this page through an AJAX call.
50+
</p>
51+
</div>
52+
{{end}}
2453
<div class="row">
2554
<div class="span12">
2655
<a href="/">Go Back</a>

samples/demo/app/views/App/Hello123Exempted.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ <h1>{{if .name}}Hello {{ .name }}{{else}}Hello anonymous user{{end}}</h1>
2121
<div class="row">
2222
<h2>This route is exempted from CSRF checks.</h2>
2323
<p>
24-
See <b>app/init.go</b>: csrf.ExemptedGlob("/Hello[0-9]?3/Exempted")
24+
See <b>github.com/cbonello/revel-csrf/samples/demo/app/init.go</b>: csrf.ExemptedGlob("/Hello[0-9]?3/Exempted")
2525
</p>
2626
</div>
2727
<div class="row">

0 commit comments

Comments
 (0)