Skip to content

Commit 5050f9c

Browse files
fjorgemotaelithrar
authored andcommitted
Add trusted origins feature (#117)
* Add trusted origins feature Closes #116 * Refactor Trusted Origins feature to be more like Django. Instead of accepting []*url.URL, which can cause weird problems, now TrustedOrigins accept []string, which is the list of hosts accepted by the middleware...So the schema doesn't matter anymore and it's more friendly to use. * Add table driven tests for the Trusted Origins feature as requested * Fix documentation of the TrustedOrigins feature * Add a section describing the Trusted Origins feature for Javascript applications on the README * Add more test cases for the table driven tests for the TrustedOrigins feature * Fix documentation of the TrustedOrigins feature so the lint error is fixed
1 parent 9b0e3ac commit 5050f9c

File tree

4 files changed

+140
-8
lines changed

4 files changed

+140
-8
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,49 @@ try {
199199
}
200200
```
201201

202+
If you plan to host your JavaScript application on another domain, you can use the Trusted Origins
203+
feature to allow the host of your JavaScript application to make requests to your Go application. Observe the example below:
204+
205+
206+
```go
207+
package main
208+
209+
import (
210+
"github.com/gorilla/csrf"
211+
"github.com/gorilla/mux"
212+
)
213+
214+
func main() {
215+
r := mux.NewRouter()
216+
csrfMiddleware := csrf.Protect([]byte("32-byte-long-auth-key"), csrf.TrustedOrigin([]string{"ui.domain.com"}))
217+
218+
api := r.PathPrefix("/api").Subrouter()
219+
api.Use(csrfMiddleware)
220+
api.HandleFunc("/user/{id}", GetUser).Methods("GET")
221+
222+
http.ListenAndServe(":8000", r)
223+
}
224+
225+
func GetUser(w http.ResponseWriter, r *http.Request) {
226+
// Authenticate the request, get the id from the route params,
227+
// and fetch the user from the DB, etc.
228+
229+
// Get the token and pass it in the CSRF header. Our JSON-speaking client
230+
// or JavaScript framework can now read the header and return the token in
231+
// in its own "X-CSRF-Token" request header on the subsequent POST.
232+
w.Header().Set("X-CSRF-Token", csrf.Token(r))
233+
b, err := json.Marshal(user)
234+
if err != nil {
235+
http.Error(w, err.Error(), 500)
236+
return
237+
}
238+
239+
w.Write(b)
240+
}
241+
```
242+
243+
On the example above, you're authorizing requests from `ui.domain.com` to make valid CSRF requests to your application, so you can have your API server on another domain without problems.
244+
202245
### Google App Engine
203246

204247
If you're using [Google App

csrf.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,13 @@ type options struct {
6666
Path string
6767
// Note that the function and field names match the case of the associated
6868
// http.Cookie field instead of the "correct" HTTPOnly name that golint suggests.
69-
HttpOnly bool
70-
Secure bool
71-
RequestHeader string
72-
FieldName string
73-
ErrorHandler http.Handler
74-
CookieName string
69+
HttpOnly bool
70+
Secure bool
71+
RequestHeader string
72+
FieldName string
73+
ErrorHandler http.Handler
74+
CookieName string
75+
TrustedOrigins []string
7576
}
7677

7778
// Protect is HTTP middleware that provides Cross-Site Request Forgery
@@ -233,7 +234,18 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
233234
return
234235
}
235236

236-
if sameOrigin(r.URL, referer) == false {
237+
valid := sameOrigin(r.URL, referer)
238+
239+
if !valid {
240+
for _, trustedOrigin := range cs.opts.TrustedOrigins {
241+
if referer.Host == trustedOrigin {
242+
valid = true
243+
break
244+
}
245+
}
246+
}
247+
248+
if valid == false {
237249
r = envError(r, ErrBadReferer)
238250
cs.opts.ErrorHandler.ServeHTTP(w, r)
239251
return

csrf_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,70 @@ func TestBadReferer(t *testing.T) {
272272
}
273273
}
274274

275+
// TestTrustedReferer checks that HTTPS requests with a Referer that does not
276+
// match the request URL correctly but is a trusted origin pass CSRF validation.
277+
func TestTrustedReferer(t *testing.T) {
278+
279+
testTable := []struct {
280+
trustedOrigin []string
281+
shouldPass bool
282+
}{
283+
{[]string{"golang.org"}, true},
284+
{[]string{"api.example.com", "golang.org"}, true},
285+
{[]string{"http://golang.org"}, false},
286+
{[]string{"https://golang.org"}, false},
287+
{[]string{"http://example.com"}, false},
288+
{[]string{"example.com"}, false},
289+
}
290+
291+
for _, item := range testTable {
292+
s := http.NewServeMux()
293+
294+
p := Protect(testKey, TrustedOrigins(item.trustedOrigin))(s)
295+
296+
var token string
297+
s.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
298+
token = Token(r)
299+
}))
300+
301+
// Obtain a CSRF cookie via a GET request.
302+
r, err := http.NewRequest("GET", "https://www.gorillatoolkit.org/", nil)
303+
if err != nil {
304+
t.Fatal(err)
305+
}
306+
307+
rr := httptest.NewRecorder()
308+
p.ServeHTTP(rr, r)
309+
310+
// POST the token back in the header.
311+
r, err = http.NewRequest("POST", "https://www.gorillatoolkit.org/", nil)
312+
if err != nil {
313+
t.Fatal(err)
314+
}
315+
316+
setCookie(rr, r)
317+
r.Header.Set("X-CSRF-Token", token)
318+
319+
// Set a non-matching Referer header.
320+
r.Header.Set("Referer", "http://golang.org/")
321+
322+
rr = httptest.NewRecorder()
323+
p.ServeHTTP(rr, r)
324+
325+
if item.shouldPass {
326+
if rr.Code != http.StatusOK {
327+
t.Fatalf("middleware failed to pass to the next handler: got %v want %v",
328+
rr.Code, http.StatusOK)
329+
}
330+
} else {
331+
if rr.Code != http.StatusForbidden {
332+
t.Fatalf("middleware failed reject a non-matching Referer header: got %v want %v",
333+
rr.Code, http.StatusForbidden)
334+
}
335+
}
336+
}
337+
}
338+
275339
// Requests with a valid Referer should pass.
276340
func TestWithReferer(t *testing.T) {
277341
s := http.NewServeMux()

options.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package csrf
22

3-
import "net/http"
3+
import (
4+
"net/http"
5+
)
46

57
// Option describes a functional option for configuring the CSRF handler.
68
type Option func(*csrf)
@@ -97,6 +99,17 @@ func CookieName(name string) Option {
9799
}
98100
}
99101

102+
// TrustedOrigins configures a set of origins (Referers) that are considered as trusted.
103+
// This will allow cross-domain CSRF use-cases - e.g. where the front-end is served
104+
// from a different domain than the API server - to correctly pass a CSRF check.
105+
//
106+
// You should only provide origins you own or have full control over.
107+
func TrustedOrigins(origins []string) Option {
108+
return func(cs *csrf) {
109+
cs.opts.TrustedOrigins = origins
110+
}
111+
}
112+
100113
// setStore sets the store used by the CSRF middleware.
101114
// Note: this is private (for now) to allow for internal API changes.
102115
func setStore(s store) Option {

0 commit comments

Comments
 (0)