Skip to content

Commit 4b50158

Browse files
tflyonselithrar
authored andcommitted
feature: Support SameSite option (#123)
* Add SameSite with build constraied * Update options test * Fix after feedback * Add docs
1 parent 7b29b05 commit 4b50158

File tree

8 files changed

+360
-2
lines changed

8 files changed

+360
-2
lines changed

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ go get github.com/gorilla/csrf
4242
- [HTML Forms](#html-forms)
4343
- [JavaScript Apps](#javascript-applications)
4444
- [Google App Engine](#google-app-engine)
45+
- [Setting SameSite](#setting-samesite)
4546
- [Setting Options](#setting-options)
4647

4748
gorilla/csrf is easy to use: add the middleware to your router with
@@ -267,6 +268,30 @@ Note: You can ignore this if you're using the
267268
[second-generation](https://cloud.google.com/appengine/docs/go/) Go runtime
268269
on App Engine (Go 1.11 and above).
269270

271+
### Setting SameSite
272+
273+
Go 1.11 introduced the option to set the SameSite attribute in cookies. This is
274+
valuable if a developer wants to instruct a browser to not include cookies during
275+
a cross site request. SameSiteStrictMode prevents all cross site requests from including
276+
the cookie. SameSiteLaxMode prevents CSRF prone requests (POST) from including the cookie
277+
but allows the cookie to be included in GET requests to support external linking.
278+
279+
```go
280+
func main() {
281+
CSRF := csrf.Protect(
282+
[]byte("a-32-byte-long-key-goes-here"),
283+
// instruct the browser to never send cookies during cross site requests
284+
csrf.SameSite(csrf.SameSiteStrictMode),
285+
)
286+
287+
r := mux.NewRouter()
288+
r.HandleFunc("/signup", GetSignupForm)
289+
r.HandleFunc("/signup/post", PostSignupForm)
290+
291+
http.ListenAndServe(":8000", CSRF(r))
292+
}
293+
```
294+
270295
### Setting Options
271296

272297
What about providing your own error handler and changing the HTTP header the
@@ -314,6 +339,9 @@ Getting CSRF protection right is important, so here's some background:
314339
- Cookies are authenticated and based on the [securecookie](https://github.com/gorilla/securecookie)
315340
library. They're also Secure (issued over HTTPS only) and are HttpOnly
316341
by default, because sane defaults are important.
342+
- Cookie SameSite attribute (prevents cookies from being sent by a browser
343+
during cross site requests) are not set by default to maintain backwards compatibility
344+
for legacy systems. The SameSite attribute can be set with the SameSite option.
317345
- Go's `crypto/rand` library is used to generate the 32 byte (256 bit) tokens
318346
and the one-time-pad used for masking them.
319347

csrf.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,22 @@ var (
5252
ErrBadToken = errors.New("CSRF token invalid")
5353
)
5454

55+
// SameSiteMode allows a server to define a cookie attribute making it impossible for
56+
// the browser to send this cookie along with cross-site requests. The main
57+
// goal is to mitigate the risk of cross-origin information leakage, and provide
58+
// some protection against cross-site request forgery attacks.
59+
//
60+
// See https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00 for details.
61+
type SameSiteMode int
62+
63+
// SameSite options
64+
const (
65+
SameSiteDefaultMode SameSiteMode = iota + 1
66+
SameSiteLaxMode
67+
SameSiteStrictMode
68+
SameSiteNoneMode
69+
)
70+
5571
type csrf struct {
5672
h http.Handler
5773
sc *securecookie.SecureCookie
@@ -68,6 +84,7 @@ type options struct {
6884
// http.Cookie field instead of the "correct" HTTPOnly name that golint suggests.
6985
HttpOnly bool
7086
Secure bool
87+
SameSite SameSiteMode
7188
RequestHeader string
7289
FieldName string
7390
ErrorHandler http.Handler
@@ -166,6 +183,7 @@ func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler {
166183
maxAge: cs.opts.MaxAge,
167184
secure: cs.opts.Secure,
168185
httpOnly: cs.opts.HttpOnly,
186+
sameSite: cs.opts.SameSite,
169187
path: cs.opts.Path,
170188
domain: cs.opts.Domain,
171189
sc: cs.sc,

options.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,26 @@ func HttpOnly(h bool) Option {
6161
}
6262
}
6363

64+
// SameSite sets the cookie SameSite attribute. Defaults to blank to maintain
65+
// backwards compatibility, however, Strict is recommended.
66+
//
67+
// SameSite(SameSiteStrictMode) will prevent the cookie from being sent by the
68+
// browser to the target site in all cross-site browsing context, even when
69+
// following a regular link (GET request).
70+
//
71+
// SameSite(SameSiteLaxMode) provides a reasonable balance between security and
72+
// usability for websites that want to maintain user's logged-in session after
73+
// the user arrives from an external link. The session cookie would be allowed
74+
// when following a regular link from an external website while blocking it in
75+
// CSRF-prone request methods (e.g. POST).
76+
//
77+
// This option is only available for go 1.11+.
78+
func SameSite(s SameSiteMode) Option {
79+
return func(cs *csrf) {
80+
cs.opts.SameSite = s
81+
}
82+
}
83+
6484
// ErrorHandler allows you to change the handler called when CSRF request
6585
// processing encounters an invalid token or request. A typical use would be to
6686
// provide a handler that returns a static HTML file with a HTTP 403 status. By
@@ -132,6 +152,9 @@ func parseOptions(h http.Handler, opts ...Option) *csrf {
132152
cs.opts.Secure = true
133153
cs.opts.HttpOnly = true
134154

155+
// Default to blank to maintain backwards compatibility
156+
cs.opts.SameSite = SameSiteDefaultMode
157+
135158
// Default; only override this if the package user explicitly calls MaxAge(0)
136159
cs.opts.MaxAge = defaultAge
137160

options_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ func TestOptions(t *testing.T) {
2424
Path(path),
2525
HttpOnly(false),
2626
Secure(false),
27+
SameSite(SameSiteStrictMode),
2728
RequestHeader(header),
2829
FieldName(field),
2930
ErrorHandler(http.HandlerFunc(errorHandler)),
@@ -53,6 +54,10 @@ func TestOptions(t *testing.T) {
5354
t.Errorf("Secure not set correctly: got %v want %v", cs.opts.Secure, false)
5455
}
5556

57+
if cs.opts.SameSite != SameSiteStrictMode {
58+
t.Errorf("SameSite not set correctly: got %v want %v", cs.opts.SameSite, SameSiteStrictMode)
59+
}
60+
5661
if cs.opts.RequestHeader != header {
5762
t.Errorf("RequestHeader not set correctly: got %v want %v", cs.opts.RequestHeader, header)
5863
}

store.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
// +build go1.11
2+
13
package csrf
24

35
import (
@@ -28,6 +30,7 @@ type cookieStore struct {
2830
path string
2931
domain string
3032
sc *securecookie.SecureCookie
33+
sameSite SameSiteMode
3134
}
3235

3336
// Get retrieves a CSRF token from the session cookie. It returns an empty token
@@ -63,6 +66,7 @@ func (cs *cookieStore) Save(token []byte, w http.ResponseWriter) error {
6366
MaxAge: cs.maxAge,
6467
HttpOnly: cs.httpOnly,
6568
Secure: cs.secure,
69+
SameSite: http.SameSite(cs.sameSite),
6670
Path: cs.path,
6771
Domain: cs.domain,
6872
}

store_legacy.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// +build !go1.11
2+
// file for compatibility with go versions prior to 1.11
3+
4+
package csrf
5+
6+
import (
7+
"net/http"
8+
"time"
9+
10+
"github.com/gorilla/securecookie"
11+
)
12+
13+
// store represents the session storage used for CSRF tokens.
14+
type store interface {
15+
// Get returns the real CSRF token from the store.
16+
Get(*http.Request) ([]byte, error)
17+
// Save stores the real CSRF token in the store and writes a
18+
// cookie to the http.ResponseWriter.
19+
// For non-cookie stores, the cookie should contain a unique (256 bit) ID
20+
// or key that references the token in the backend store.
21+
// csrf.GenerateRandomBytes is a helper function for generating secure IDs.
22+
Save(token []byte, w http.ResponseWriter) error
23+
}
24+
25+
// cookieStore is a signed cookie session store for CSRF tokens.
26+
type cookieStore struct {
27+
name string
28+
maxAge int
29+
secure bool
30+
httpOnly bool
31+
path string
32+
domain string
33+
sc *securecookie.SecureCookie
34+
sameSite SameSiteMode
35+
}
36+
37+
// Get retrieves a CSRF token from the session cookie. It returns an empty token
38+
// if decoding fails (e.g. HMAC validation fails or the named cookie doesn't exist).
39+
func (cs *cookieStore) Get(r *http.Request) ([]byte, error) {
40+
// Retrieve the cookie from the request
41+
cookie, err := r.Cookie(cs.name)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
token := make([]byte, tokenLength)
47+
// Decode the HMAC authenticated cookie.
48+
err = cs.sc.Decode(cs.name, cookie.Value, &token)
49+
if err != nil {
50+
return nil, err
51+
}
52+
53+
return token, nil
54+
}
55+
56+
// Save stores the CSRF token in the session cookie.
57+
func (cs *cookieStore) Save(token []byte, w http.ResponseWriter) error {
58+
// Generate an encoded cookie value with the CSRF token.
59+
encoded, err := cs.sc.Encode(cs.name, token)
60+
if err != nil {
61+
return err
62+
}
63+
64+
cookie := &http.Cookie{
65+
Name: cs.name,
66+
Value: encoded,
67+
MaxAge: cs.maxAge,
68+
HttpOnly: cs.httpOnly,
69+
Secure: cs.secure,
70+
Path: cs.path,
71+
Domain: cs.domain,
72+
}
73+
74+
// Set the Expires field on the cookie based on the MaxAge
75+
// If MaxAge <= 0, we don't set the Expires attribute, making the cookie
76+
// session-only.
77+
if cs.maxAge > 0 {
78+
cookie.Expires = time.Now().Add(
79+
time.Duration(cs.maxAge) * time.Second)
80+
}
81+
82+
// Write the authenticated cookie to the response.
83+
http.SetCookie(w, cookie)
84+
85+
return nil
86+
}

0 commit comments

Comments
 (0)