├── .gitignore ├── LICENSE ├── README.md ├── csrf.go ├── csrf_test.go ├── examples ├── angular │ ├── public │ │ └── main.js │ ├── server.go │ └── templates │ │ ├── index.html │ │ └── login.tmpl └── server_rendered │ ├── server.go │ └── templates │ ├── custom_error.tmpl │ ├── error.html │ ├── index.tmpl │ ├── login.tmpl │ └── result.tmpl └── wercker.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Martini Contrib 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | csrf [![wercker status](https://app.wercker.com/status/ba1aa8d0a0e9c990bff5ceb06af4bc33/s/ "wercker status")](https://app.wercker.com/project/bykey/ba1aa8d0a0e9c990bff5ceb06af4bc33) 2 | ==== 3 | 4 | Martini cross-site request forgery protection middlware. 5 | 6 | [API Reference](http://godoc.org/github.com/martini-contrib/csrf) 7 | 8 | ## Usage 9 | 10 | ~~~ go 11 | 12 | package main 13 | 14 | import ( 15 | "github.com/go-martini/martini" 16 | "github.com/martini-contrib/csrf" 17 | "github.com/martini-contrib/sessions" 18 | "github.com/martini-contrib/render" 19 | "net/http" 20 | ) 21 | 22 | func main() { 23 | m := martini.Classic() 24 | store := sessions.NewCookieStore([]byte("secret123")) 25 | m.Use(sessions.Sessions("my_session", store)) 26 | // Setup generation middleware. 27 | m.Use(csrf.Generate(&csrf.Options{ 28 | Secret: "token123", 29 | SessionKey: "userID", 30 | // Custom error response. 31 | ErrorFunc: func(w http.ResponseWriter) { 32 | http.Error(w, "CSRF token validation failed", http.StatusBadRequest) 33 | } 34 | })) 35 | m.Use(render.Renderer()) 36 | 37 | // Simulate the authentication of a session. If userID exists redirect 38 | // to a form that requires csrf protection. 39 | m.Get("/", func(s sessions.Session, r render.Render) { 40 | if s.Get("userID") == nil { 41 | r.Redirect("/login", 302) 42 | return 43 | } 44 | r.Redirect("/protected", 302) 45 | }) 46 | 47 | // Set userID for the session. 48 | m.Get("/login", func(s sessions.Session, r render.Render) { 49 | s.Set("userID", "123456") 50 | r.Redirect("/", 302) 51 | }) 52 | 53 | // Render a protected form. Passing a csrf token by calling x.GetToken() 54 | m.Get("/protected", func(s sessions.Session, r render.Render, x csrf.CSRF) { 55 | if s.Get("userID") == nil { 56 | r.Redirect("/login", 401) 57 | return 58 | } 59 | // Pass token to the protected template. 60 | r.HTML(200, "protected", x.GetToken()) 61 | }) 62 | 63 | // Apply csrf validation to route. 64 | m.Post("/protected", csrf.Validate, func(s sessions.Session, r render.Render) { 65 | if s.Get("userID") != nil { 66 | r.HTML(200, "result", "You submitted a valid token") 67 | return 68 | } 69 | r.Redirect("/login", 401) 70 | }) 71 | 72 | m.Run() 73 | } 74 | 75 | ~~~ 76 | 77 | ## Security 78 | Applications using the [method](https://github.com/martini-contrib/method) package should also validate PATCH, PUT, and DELETE requests. 79 | 80 | ## Authors 81 | * [Tom Steele](http://github.com/tomsteele) 82 | -------------------------------------------------------------------------------- /csrf.go: -------------------------------------------------------------------------------- 1 | // Package csrf generates and validates csrf tokens for martini. 2 | // There are multiple methods of delivery including via a cookie or HTTP 3 | // header. 4 | // Validation occurs via a traditional hidden form key of "_csrf", or via 5 | // a custom HTTP header "X-CSRFToken". 6 | // 7 | // package main 8 | // 9 | // import ( 10 | // "github.com/go-martini/martini" 11 | // "github.com/martini-contib/csrf" 12 | // "github.com/martini-contrib/render" 13 | // "github.com/martini-contib/sessions" 14 | // "net/http" 15 | // ) 16 | // 17 | // func main() { 18 | // m := martini.Classic() 19 | // store := sessions.NewCookieStore([]byte("secret123")) 20 | // m.Use(sessions.Sessions("my_session", store)) 21 | // // Setup generation middleware. 22 | // m.Use(csrf.Generate(&csrf.Options{ 23 | // Secret: "token123", 24 | // SessionKey: "userID", 25 | // })) 26 | // m.Use(render.Renderer()) 27 | // 28 | // // Simulate the authentication of a session. If userID exists redirect 29 | // // to a form that requires csrf protection. 30 | // m.Get("/", func(s sessions.Session, r render.Render) { 31 | // if s.Get("userID") == nil { 32 | // r.Redirect("/login", 302) 33 | // return 34 | // } 35 | // r.Redirect("/protected", 302) 36 | // }) 37 | // 38 | // // Set userID for the session. 39 | // m.Get("/login", func(s sessions.Session, r render.Render) { 40 | // s.Set("userID", "123456") 41 | // r.Redirect("/", 302) 42 | // }) 43 | // 44 | // // Render a protected form. Passing a csrf token by calling x.GetToken() 45 | // m.Get("/protected", func(s sessions.Session, r render.Render, x csrf.CSRF) { 46 | // if s.Get("userID") == nil { 47 | // r.Redirect("/login", 401) 48 | // return 49 | // } 50 | // r.HTML(200, "protected", x.GetToken()) 51 | // }) 52 | // 53 | // // Apply csrf validation to route. 54 | // m.Post("/protected", csrf.Validate, func(s sessions.Session, r render.Render) { 55 | // if s.Get("userID") != nil { 56 | // r.HTML(200, "result", "You submitted a valid token") 57 | // return 58 | // } 59 | // r.Redirect("/login", 401) 60 | // }) 61 | // 62 | // m.Run() 63 | // } 64 | package csrf 65 | 66 | import ( 67 | "fmt" 68 | "net/http" 69 | "net/url" 70 | "regexp" 71 | "strconv" 72 | "strings" 73 | "time" 74 | 75 | "github.com/go-martini/martini" 76 | "github.com/martini-contrib/sessions" 77 | "golang.org/x/net/xsrftoken" 78 | ) 79 | 80 | // CSRF is used to get the current token and validate a suspect token. 81 | type CSRF interface { 82 | // Return HTTP header to search for token. 83 | GetHeaderName() string 84 | // Return form value to search for token. 85 | GetFormName() string 86 | // Return cookie name to search for token. 87 | GetCookieName() string 88 | // Return the token. 89 | GetToken() string 90 | // Validate by token. 91 | ValidToken(t string) bool 92 | // Error replies to the request with a custom function when ValidToken fails. 93 | Error(w http.ResponseWriter) 94 | } 95 | 96 | type csrf struct { 97 | // Header name value for setting and getting csrf token. 98 | Header string 99 | // Form name value for setting and getting csrf token. 100 | Form string 101 | // Cookie name value for setting and getting csrf token. 102 | Cookie string 103 | // Token generated to pass via header, cookie, or hidden form value. 104 | Token string 105 | // This value must be unique per user. 106 | ID string 107 | // Secret used along with the unique id above to generate the Token. 108 | Secret string 109 | // ErrorFunc is the custom function that replies to the request when ValidToken fails. 110 | ErrorFunc func(w http.ResponseWriter) 111 | } 112 | 113 | // Returns the name of the HTTP header for csrf token. 114 | func (c *csrf) GetHeaderName() string { 115 | return c.Header 116 | } 117 | 118 | // Returns the name of the form value for csrf token. 119 | func (c *csrf) GetFormName() string { 120 | return c.Form 121 | } 122 | 123 | // Returns the name of the cookie for csrf token. 124 | func (c *csrf) GetCookieName() string { 125 | return c.Cookie 126 | } 127 | 128 | // Returns the current token. This is typically used 129 | // to populate a hidden form in an HTML template. 130 | func (c *csrf) GetToken() string { 131 | return c.Token 132 | } 133 | 134 | // Validates the passed token against the existing Secret and ID. 135 | func (c *csrf) ValidToken(t string) bool { 136 | return xsrftoken.Valid(t, c.Secret, c.ID, "POST") 137 | } 138 | 139 | // Error replies to the request when ValidToken fails. 140 | func (c *csrf) Error(w http.ResponseWriter) { 141 | c.ErrorFunc(w) 142 | } 143 | 144 | // Options maintains options to manage behavior of Generate. 145 | type Options struct { 146 | // The global secret value used to generate Tokens. 147 | Secret string 148 | // HTTP header used to set and get token. 149 | Header string 150 | // Form value used to set and get token. 151 | Form string 152 | // Cookie value used to set and get token. 153 | Cookie string 154 | // Key used for getting the unique ID per user. 155 | SessionKey string 156 | // If true, send token via X-CSRFToken header. 157 | SetHeader bool 158 | // If true, send token via _csrf cookie. 159 | SetCookie bool 160 | // Set the Secure flag to true on the cookie. 161 | Secure bool 162 | // The function called when Validate fails. 163 | ErrorFunc func(w http.ResponseWriter) 164 | // Array of allowed origins. Will be checked during generation from a cross site request. 165 | // Must be the complete origin. Example: 'https://golang.org'. You will only need to set this 166 | // if you are supporting CORS. 167 | AllowedOrigins []string 168 | } 169 | 170 | const domainReg = `^\.?[a-z\d]+(?:(?:[a-z\d]*)|(?:[a-z\d\-]*[a-z\d]))(?:\.[a-z\d]+(?:(?:[a-z\d]*)|(?:[a-z\d\-]*[a-z\d])))*$` 171 | 172 | // Generate maps CSRF to each request. If this request is a Get request, it will generate a new token. 173 | // Additionally, depending on options set, generated tokens will be sent via Header and/or Cookie. 174 | func Generate(opts *Options) martini.Handler { 175 | return func(s sessions.Session, c martini.Context, r *http.Request, w http.ResponseWriter) { 176 | if opts.Header == "" { 177 | opts.Header = "X-CSRFToken" 178 | } 179 | if opts.Form == "" { 180 | opts.Form = "_csrf" 181 | } 182 | if opts.Cookie == "" { 183 | opts.Cookie = "_csrf" 184 | } 185 | if opts.ErrorFunc == nil { 186 | opts.ErrorFunc = func(w http.ResponseWriter) { 187 | http.Error(w, "Invalid csrf token.", http.StatusBadRequest) 188 | } 189 | } 190 | 191 | x := &csrf{ 192 | Secret: opts.Secret, 193 | Header: opts.Header, 194 | Form: opts.Form, 195 | Cookie: opts.Cookie, 196 | ErrorFunc: opts.ErrorFunc, 197 | } 198 | c.MapTo(x, (*CSRF)(nil)) 199 | 200 | uid := s.Get(opts.SessionKey) 201 | if uid == nil { 202 | return 203 | } 204 | switch uid.(type) { 205 | case string: 206 | x.ID = uid.(string) 207 | case int64: 208 | x.ID = strconv.FormatInt(uid.(int64), 10) 209 | default: 210 | return 211 | } 212 | 213 | if r.Header.Get("Origin") != "" { 214 | originUrl, err := url.Parse(r.Header.Get("Origin")) 215 | if err != nil { 216 | return 217 | } 218 | if originUrl.Host != r.Host { 219 | isAllowed := false 220 | for _, origin := range opts.AllowedOrigins { 221 | if originUrl.String() == origin { 222 | isAllowed = true 223 | break 224 | } 225 | } 226 | if !isAllowed { 227 | return 228 | } 229 | } 230 | } 231 | 232 | // If cookie present, map existing token, else generate a new one. 233 | if ex, err := r.Cookie(opts.Cookie); err == nil && ex.Value != "" { 234 | x.Token = ex.Value 235 | } else { 236 | x.Token = xsrftoken.Generate(x.Secret, x.ID, "POST") 237 | if opts.SetCookie { 238 | expire := time.Now().AddDate(0, 0, 1) 239 | // Verify the domain is valid. If it is not, set as empty. 240 | domain := strings.Split(r.Host, ":")[0] 241 | if ok, err := regexp.Match(domainReg, []byte(domain)); !ok || err != nil { 242 | domain = "" 243 | } 244 | 245 | cookie := &http.Cookie{ 246 | Name: opts.Cookie, 247 | Value: x.Token, 248 | Path: "/", 249 | Domain: domain, 250 | Expires: expire, 251 | RawExpires: expire.Format(time.UnixDate), 252 | MaxAge: 0, 253 | Secure: opts.Secure, 254 | HttpOnly: false, 255 | Raw: fmt.Sprintf("%s=%s", opts.Cookie, x.Token), 256 | Unparsed: []string{fmt.Sprintf("token=%s", x.Token)}, 257 | } 258 | http.SetCookie(w, cookie) 259 | } 260 | } 261 | 262 | if opts.SetHeader { 263 | w.Header().Add(opts.Header, x.Token) 264 | } 265 | } 266 | 267 | } 268 | 269 | // Validate should be used as a per route middleware. It attempts to get a token from a "X-CSRFToken" 270 | // HTTP header and then a "_csrf" form value. If one of these is found, the token will be validated 271 | // using ValidToken. If this validation fails, custom Error is sent in the reply. 272 | // If neither a header or form value is found, http.StatusBadRequest is sent. 273 | func Validate(r *http.Request, w http.ResponseWriter, x CSRF) { 274 | if token := r.Header.Get(x.GetHeaderName()); token != "" { 275 | if !x.ValidToken(token) { 276 | x.Error(w) 277 | } 278 | return 279 | } 280 | if token := r.FormValue(x.GetFormName()); token != "" { 281 | if !x.ValidToken(token) { 282 | x.Error(w) 283 | } 284 | return 285 | } 286 | 287 | http.Error(w, "Bad Request", http.StatusBadRequest) 288 | return 289 | } 290 | -------------------------------------------------------------------------------- /csrf_test.go: -------------------------------------------------------------------------------- 1 | package csrf 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "strconv" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/go-martini/martini" 13 | "github.com/martini-contrib/sessions" 14 | ) 15 | 16 | func Test_GenerateToken(t *testing.T) { 17 | m := martini.Classic() 18 | store := sessions.NewCookieStore([]byte("secret123")) 19 | m.Use(sessions.Sessions("my_session", store)) 20 | m.Use(Generate(&Options{ 21 | Secret: "token123", 22 | SessionKey: "userID", 23 | })) 24 | 25 | // Simulate login. 26 | m.Get("/login", func(s sessions.Session) string { 27 | s.Set("userID", "123456") 28 | return "OK" 29 | }) 30 | 31 | // Generate token. 32 | m.Get("/private", func(s sessions.Session, x CSRF) string { 33 | return x.GetToken() 34 | }) 35 | 36 | res := httptest.NewRecorder() 37 | req, _ := http.NewRequest("GET", "/login", nil) 38 | m.ServeHTTP(res, req) 39 | 40 | res2 := httptest.NewRecorder() 41 | req2, _ := http.NewRequest("GET", "/private", nil) 42 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 43 | m.ServeHTTP(res2, req2) 44 | 45 | if res2.Body.String() == "" { 46 | t.Error("Failed to generate token") 47 | } 48 | } 49 | 50 | func Test_GenerateCookie(t *testing.T) { 51 | m := martini.Classic() 52 | store := sessions.NewCookieStore([]byte("secret123")) 53 | m.Use(sessions.Sessions("my_session", store)) 54 | m.Use(Generate(&Options{ 55 | Secret: "token123", 56 | SessionKey: "userID", 57 | SetCookie: true, 58 | })) 59 | 60 | // Simulate login. 61 | m.Get("/login", func(s sessions.Session) string { 62 | s.Set("userID", "123456") 63 | return "OK" 64 | }) 65 | 66 | // Generate cookie. 67 | m.Get("/private", func(s sessions.Session, x CSRF) string { 68 | return "OK" 69 | }) 70 | 71 | res := httptest.NewRecorder() 72 | req, _ := http.NewRequest("GET", "/login", nil) 73 | m.ServeHTTP(res, req) 74 | 75 | res2 := httptest.NewRecorder() 76 | req2, _ := http.NewRequest("GET", "/private", nil) 77 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 78 | m.ServeHTTP(res2, req2) 79 | 80 | if !strings.Contains(res2.Header().Get("Set-Cookie"), "_csrf") { 81 | t.Error("Failed to set csrf cookie") 82 | } 83 | } 84 | 85 | func Test_GenerateCustomCookie(t *testing.T) { 86 | m := martini.Classic() 87 | store := sessions.NewCookieStore([]byte("secret123")) 88 | m.Use(sessions.Sessions("my_session", store)) 89 | m.Use(Generate(&Options{ 90 | Secret: "token123", 91 | SessionKey: "userID", 92 | SetCookie: true, 93 | Cookie: "seesurf", 94 | })) 95 | 96 | // Simulate login. 97 | m.Get("/login", func(s sessions.Session) string { 98 | s.Set("userID", "123456") 99 | return "OK" 100 | }) 101 | 102 | // Generate cookie. 103 | m.Get("/private", func(s sessions.Session, x CSRF) string { 104 | return "OK" 105 | }) 106 | 107 | res := httptest.NewRecorder() 108 | req, _ := http.NewRequest("GET", "/login", nil) 109 | m.ServeHTTP(res, req) 110 | 111 | res2 := httptest.NewRecorder() 112 | req2, _ := http.NewRequest("GET", "/private", nil) 113 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 114 | m.ServeHTTP(res2, req2) 115 | 116 | if !strings.Contains(res2.Header().Get("Set-Cookie"), "seesurf") { 117 | t.Error("Failed to set custom csrf cookie") 118 | } 119 | } 120 | 121 | func Test_GenerateHeader(t *testing.T) { 122 | m := martini.Classic() 123 | store := sessions.NewCookieStore([]byte("secret123")) 124 | m.Use(sessions.Sessions("my_session", store)) 125 | m.Use(Generate(&Options{ 126 | Secret: "token123", 127 | SessionKey: "userID", 128 | SetHeader: true, 129 | })) 130 | 131 | // Simulate login. 132 | m.Get("/login", func(s sessions.Session) string { 133 | s.Set("userID", "123456") 134 | return "OK" 135 | }) 136 | 137 | // Generate HTTP header. 138 | m.Get("/private", func(s sessions.Session, x CSRF) string { 139 | return "OK" 140 | }) 141 | 142 | res := httptest.NewRecorder() 143 | req, _ := http.NewRequest("GET", "/login", nil) 144 | m.ServeHTTP(res, req) 145 | 146 | res2 := httptest.NewRecorder() 147 | req2, _ := http.NewRequest("GET", "/private", nil) 148 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 149 | m.ServeHTTP(res2, req2) 150 | 151 | if res2.Header().Get("X-CSRFToken") == "" { 152 | t.Error("Failed to set X-CSRFToken header") 153 | } 154 | } 155 | func Test_SameOriginHeader(t *testing.T) { 156 | m := martini.Classic() 157 | store := sessions.NewCookieStore([]byte("secret123")) 158 | m.Use(sessions.Sessions("my_session", store)) 159 | m.Use(Generate(&Options{ 160 | Secret: "token123", 161 | SessionKey: "userID", 162 | SetHeader: true, 163 | })) 164 | 165 | // Simulate login. 166 | m.Get("/login", func(s sessions.Session) string { 167 | s.Set("userID", "123456") 168 | return "OK" 169 | }) 170 | 171 | // Generate HTTP header. 172 | m.Get("/private", func(s sessions.Session, x CSRF) string { 173 | return "OK" 174 | }) 175 | 176 | res := httptest.NewRecorder() 177 | req, _ := http.NewRequest("GET", "/login", nil) 178 | m.ServeHTTP(res, req) 179 | 180 | res2 := httptest.NewRecorder() 181 | req2, _ := http.NewRequest("GET", "/private", nil) 182 | req2.Host = "localhost:3000" 183 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 184 | req2.Header.Set("Origin", "https://localhost:3000") 185 | m.ServeHTTP(res2, req2) 186 | 187 | if res2.Header().Get("X-CSRFToken") == "" { 188 | t.Error("X-CSRFToken not present in same origin request") 189 | } 190 | } 191 | 192 | func Test_NotOriginHeader(t *testing.T) { 193 | m := martini.Classic() 194 | store := sessions.NewCookieStore([]byte("secret123")) 195 | m.Use(sessions.Sessions("my_session", store)) 196 | m.Use(Generate(&Options{ 197 | Secret: "token123", 198 | SessionKey: "userID", 199 | SetHeader: true, 200 | })) 201 | 202 | // Simulate login. 203 | m.Get("/login", func(s sessions.Session) string { 204 | s.Set("userID", "123456") 205 | return "OK" 206 | }) 207 | 208 | // Generate HTTP header. 209 | m.Get("/private", func(s sessions.Session, x CSRF) string { 210 | return "OK" 211 | }) 212 | 213 | res := httptest.NewRecorder() 214 | req, _ := http.NewRequest("GET", "/login", nil) 215 | m.ServeHTTP(res, req) 216 | 217 | res2 := httptest.NewRecorder() 218 | req2, _ := http.NewRequest("GET", "/private", nil) 219 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 220 | req2.Header.Set("Origin", "https://www.example.com") 221 | m.ServeHTTP(res2, req2) 222 | 223 | if res2.Header().Get("X-CSRFToken") != "" { 224 | t.Error("X-CSRFToken present in cross origin request") 225 | } 226 | } 227 | 228 | func Test_AllowedOriginHeader(t *testing.T) { 229 | m := martini.Classic() 230 | store := sessions.NewCookieStore([]byte("secret123")) 231 | m.Use(sessions.Sessions("my_session", store)) 232 | m.Use(Generate(&Options{ 233 | Secret: "token123", 234 | SessionKey: "userID", 235 | SetHeader: true, 236 | AllowedOrigins: []string{"https://www.example.com"}, 237 | })) 238 | 239 | // Simulate login. 240 | m.Get("/login", func(s sessions.Session) string { 241 | s.Set("userID", "123456") 242 | return "OK" 243 | }) 244 | 245 | // Generate HTTP header. 246 | m.Get("/private", func(s sessions.Session, x CSRF) string { 247 | return "OK" 248 | }) 249 | 250 | res := httptest.NewRecorder() 251 | req, _ := http.NewRequest("GET", "/login", nil) 252 | m.ServeHTTP(res, req) 253 | 254 | res2 := httptest.NewRecorder() 255 | req2, _ := http.NewRequest("GET", "/private", nil) 256 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 257 | req2.Header.Set("Origin", "https://www.example.com") 258 | m.ServeHTTP(res2, req2) 259 | 260 | if res2.Header().Get("X-CSRFToken") == "" { 261 | t.Error("X-CSRFToken not present in allowed origin request") 262 | } 263 | } 264 | 265 | func Test_NotAllowedOriginHeader(t *testing.T) { 266 | m := martini.Classic() 267 | store := sessions.NewCookieStore([]byte("secret123")) 268 | m.Use(sessions.Sessions("my_session", store)) 269 | m.Use(Generate(&Options{ 270 | Secret: "token123", 271 | SessionKey: "userID", 272 | SetHeader: true, 273 | AllowedOrigins: []string{"http://www.example.com"}, 274 | })) 275 | 276 | // Simulate login. 277 | m.Get("/login", func(s sessions.Session) string { 278 | s.Set("userID", "123456") 279 | return "OK" 280 | }) 281 | 282 | // Generate HTTP header. 283 | m.Get("/private", func(s sessions.Session, x CSRF) string { 284 | return "OK" 285 | }) 286 | 287 | res := httptest.NewRecorder() 288 | req, _ := http.NewRequest("GET", "/login", nil) 289 | m.ServeHTTP(res, req) 290 | 291 | res2 := httptest.NewRecorder() 292 | req2, _ := http.NewRequest("GET", "/private", nil) 293 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 294 | req2.Header.Set("Origin", "https://www.example.com") 295 | m.ServeHTTP(res2, req2) 296 | 297 | if res2.Header().Get("X-CSRFToken") != "" { 298 | t.Error("X-CSRFToken present in allowed origin request") 299 | } 300 | } 301 | func Test_GenerateCustomHeader(t *testing.T) { 302 | m := martini.Classic() 303 | store := sessions.NewCookieStore([]byte("secret123")) 304 | m.Use(sessions.Sessions("my_session", store)) 305 | m.Use(Generate(&Options{ 306 | Secret: "token123", 307 | SessionKey: "userID", 308 | SetHeader: true, 309 | Header: "X-SEESurfToken", 310 | })) 311 | 312 | // Simulate login. 313 | m.Get("/login", func(s sessions.Session) string { 314 | s.Set("userID", "123456") 315 | return "OK" 316 | }) 317 | 318 | // Generate HTTP header. 319 | m.Get("/private", func(s sessions.Session, x CSRF) string { 320 | return "OK" 321 | }) 322 | 323 | res := httptest.NewRecorder() 324 | req, _ := http.NewRequest("GET", "/login", nil) 325 | m.ServeHTTP(res, req) 326 | 327 | res2 := httptest.NewRecorder() 328 | req2, _ := http.NewRequest("GET", "/private", nil) 329 | req2.Header.Set("Cookie", res.Header().Get("Set-Cookie")) 330 | m.ServeHTTP(res2, req2) 331 | 332 | if res2.Header().Get("X-SEESurfToken") == "" { 333 | t.Error("Failed to set X-SEESurfToken custom header") 334 | } 335 | } 336 | 337 | func Test_Validate(t *testing.T) { 338 | m := martini.Classic() 339 | store := sessions.NewCookieStore([]byte("secret123")) 340 | m.Use(sessions.Sessions("my_session", store)) 341 | m.Use(Generate(&Options{ 342 | Secret: "token123", 343 | SessionKey: "userID", 344 | })) 345 | 346 | // Simulate login. 347 | m.Get("/login", func(s sessions.Session) string { 348 | s.Set("userID", "123456") 349 | return "OK" 350 | }) 351 | 352 | // Generate token. 353 | m.Get("/private", func(s sessions.Session, x CSRF) string { 354 | return x.GetToken() 355 | }) 356 | 357 | m.Post("/private", Validate, func(s sessions.Session) string { 358 | return "OK" 359 | }) 360 | 361 | // Login to set session. 362 | res := httptest.NewRecorder() 363 | req, _ := http.NewRequest("GET", "/login", nil) 364 | m.ServeHTTP(res, req) 365 | 366 | cookie := res.Header().Get("Set-Cookie") 367 | 368 | // Get a new token. 369 | res2 := httptest.NewRecorder() 370 | req2, _ := http.NewRequest("GET", "/private", nil) 371 | req2.Header.Set("Cookie", cookie) 372 | m.ServeHTTP(res2, req2) 373 | 374 | // Post using _csrf form value. 375 | data := url.Values{} 376 | data.Set("_csrf", res2.Body.String()) 377 | res3 := httptest.NewRecorder() 378 | req3, _ := http.NewRequest("POST", "/private", bytes.NewBufferString(data.Encode())) 379 | req3.Header.Set("Content-Type", "application/x-www-form-urlencoded") 380 | req3.Header.Set("Content-Length", strconv.Itoa(len(data.Encode()))) 381 | req3.Header.Set("Cookie", cookie) 382 | m.ServeHTTP(res3, req3) 383 | if res3.Code == 400 { 384 | t.Error("Validation of _csrf form value failed") 385 | } 386 | 387 | // Post using X-CSRFToken HTTP header. 388 | res4 := httptest.NewRecorder() 389 | req4, _ := http.NewRequest("POST", "/private", nil) 390 | req4.Header.Set("X-CSRFToken", res2.Body.String()) 391 | req4.Header.Set("Cookie", cookie) 392 | m.ServeHTTP(res4, req4) 393 | if res4.Code == 400 { 394 | t.Error("Validation of X-CSRFToken failed") 395 | } 396 | } 397 | 398 | func Test_ValidateCustom(t *testing.T) { 399 | m := martini.Classic() 400 | store := sessions.NewCookieStore([]byte("secret123")) 401 | m.Use(sessions.Sessions("my_session", store)) 402 | m.Use(Generate(&Options{ 403 | Secret: "token123", 404 | SessionKey: "userID", 405 | Header: "X-SEESurfToken", 406 | Form: "_seesurf", 407 | })) 408 | 409 | // Simulate login. 410 | m.Get("/login", func(s sessions.Session) string { 411 | s.Set("userID", "123456") 412 | return "OK" 413 | }) 414 | 415 | // Generate token. 416 | m.Get("/private", func(s sessions.Session, x CSRF) string { 417 | return x.GetToken() 418 | }) 419 | 420 | m.Post("/private", Validate, func(s sessions.Session) string { 421 | return "OK" 422 | }) 423 | 424 | // Login to set session. 425 | res := httptest.NewRecorder() 426 | req, _ := http.NewRequest("GET", "/login", nil) 427 | m.ServeHTTP(res, req) 428 | 429 | cookie := res.Header().Get("Set-Cookie") 430 | 431 | // Get a new token. 432 | res2 := httptest.NewRecorder() 433 | req2, _ := http.NewRequest("GET", "/private", nil) 434 | req2.Header.Set("Cookie", cookie) 435 | m.ServeHTTP(res2, req2) 436 | 437 | // Post using custom form value. 438 | data := url.Values{} 439 | data.Set("_seesurf", res2.Body.String()) 440 | res3 := httptest.NewRecorder() 441 | req3, _ := http.NewRequest("POST", "/private", bytes.NewBufferString(data.Encode())) 442 | req3.Header.Set("Content-Type", "application/x-www-form-urlencoded") 443 | req3.Header.Set("Content-Length", strconv.Itoa(len(data.Encode()))) 444 | req3.Header.Set("Cookie", cookie) 445 | m.ServeHTTP(res3, req3) 446 | if res3.Code == 400 { 447 | t.Error("Valiation of _seesurf custom form value failed") 448 | } 449 | 450 | // Post using custom HTTP header. 451 | res4 := httptest.NewRecorder() 452 | req4, _ := http.NewRequest("POST", "/private", nil) 453 | req4.Header.Set("X-SEESurfToken", res2.Body.String()) 454 | req4.Header.Set("Cookie", cookie) 455 | m.ServeHTTP(res4, req4) 456 | if res4.Code == 400 { 457 | t.Error("Validation of X-SEESurfToken custom header value failed") 458 | } 459 | } 460 | 461 | func Test_ValidateCustomError(t *testing.T) { 462 | m := martini.Classic() 463 | store := sessions.NewCookieStore([]byte("secret123")) 464 | m.Use(sessions.Sessions("my_session", store)) 465 | m.Use(Generate(&Options{ 466 | Secret: "token123", 467 | SessionKey: "userID", 468 | ErrorFunc: func(w http.ResponseWriter) { 469 | http.Error(w, "custom error", 422) 470 | }, 471 | })) 472 | 473 | // Simulate login. 474 | m.Get("/login", func(s sessions.Session) string { 475 | s.Set("userID", "123456") 476 | return "OK" 477 | }) 478 | 479 | // Generate token. 480 | m.Get("/private", func(s sessions.Session, x CSRF) string { 481 | return x.GetToken() 482 | }) 483 | 484 | m.Post("/private", Validate, func(s sessions.Session) string { 485 | return "OK" 486 | }) 487 | 488 | // Login to set session. 489 | res := httptest.NewRecorder() 490 | req, _ := http.NewRequest("GET", "/login", nil) 491 | m.ServeHTTP(res, req) 492 | 493 | cookie := res.Header().Get("Set-Cookie") 494 | 495 | // Get a new token. 496 | res2 := httptest.NewRecorder() 497 | req2, _ := http.NewRequest("GET", "/private", nil) 498 | req2.Header.Set("Cookie", cookie) 499 | m.ServeHTTP(res2, req2) 500 | 501 | // Post using _csrf form value. 502 | data := url.Values{} 503 | data.Set("_csrf", "invalid") 504 | res3 := httptest.NewRecorder() 505 | req3, _ := http.NewRequest("POST", "/private", bytes.NewBufferString(data.Encode())) 506 | req3.Header.Set("Content-Type", "application/x-www-form-urlencoded") 507 | req3.Header.Set("Content-Length", strconv.Itoa(len(data.Encode()))) 508 | m.ServeHTTP(res3, req3) 509 | if res3.Code != 422 { 510 | t.Errorf("Custom error response code failed: %d", res3.Code) 511 | } 512 | if res3.Body.String() != "custom error\n" { 513 | t.Errorf("Custom error response body failed: %s", res3.Body) 514 | } 515 | 516 | // Post using X-CSRFToken HTTP header. 517 | res4 := httptest.NewRecorder() 518 | req4, _ := http.NewRequest("POST", "/private", nil) 519 | req4.Header.Set("X-CSRFToken", "invalid") 520 | m.ServeHTTP(res4, req4) 521 | if res4.Code != 422 { 522 | t.Errorf("Custom error response code failed: %d", res4.Code) 523 | } 524 | if res4.Body.String() != "custom error\n" { 525 | t.Errorf("Custom error response body failed: %s", res4.Body) 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /examples/angular/public/main.js: -------------------------------------------------------------------------------- 1 | var app = angular.module('app', ['ngCookies']). 2 | run(function($http, $cookies) { 3 | // Add the header to every post request. 4 | // Real code will need to be more robust. 5 | $http.defaults.headers.post['X-CSRFToken'] = $cookies._csrf; 6 | }); 7 | 8 | app.controller('protected', function($scope, $http) { 9 | $scope.result = 'In progress'; 10 | $http.post('/protected', {}). 11 | success(function(data) { 12 | $scope.result = data.message; 13 | }). 14 | error(function() { 15 | $scope.result = "Could not complete protected action!"; 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /examples/angular/server.go: -------------------------------------------------------------------------------- 1 | // Simple angular.js example. Simulates authentication and sends csrf 2 | // token in cookie. Angular.js is then configured to pull the token 3 | // from the cookie and send as X-CSRFToken header. 4 | 5 | package main 6 | 7 | import ( 8 | "github.com/go-martini/martini" 9 | "github.com/martini-contrib/csrf" 10 | "github.com/martini-contrib/render" 11 | "github.com/martini-contrib/sessions" 12 | "net/http" 13 | ) 14 | 15 | func main() { 16 | m := martini.Classic() 17 | store := sessions.NewCookieStore([]byte("secret123")) 18 | m.Use(render.Renderer()) 19 | m.Use(sessions.Sessions("my_session", store)) 20 | // Send token as a cookie. 21 | m.Use(csrf.Generate(&csrf.Options{ 22 | Secret: "token123", 23 | SessionKey: "userID", 24 | SetCookie: true, 25 | })) 26 | 27 | // Simulate a typical authentication example. If the user has a valid userID render index.html 28 | // else redirect to "/login". 29 | m.Get("/", func(s sessions.Session, r render.Render, req *http.Request, resp http.ResponseWriter) { 30 | if u := s.Get("userID"); u == nil { 31 | r.Redirect("/login", 302) 32 | return 33 | } 34 | // Token will be generated here. Using ServeFile for lazy angular loading. 35 | http.ServeFile(resp, req, "templates/index.html") 36 | }) 37 | 38 | m.Get("/login", func(r render.Render) { 39 | r.HTML(200, "login", nil) 40 | }) 41 | 42 | // Simulate a valid login by setting a bogus session id. 43 | m.Post("/login", func(s sessions.Session, r render.Render) { 44 | s.Set("userID", "123456789") 45 | r.Redirect("/", 302) 46 | }) 47 | 48 | // csrf.Validate requires a proper token. 49 | m.Post("/protected", csrf.Validate, func(r render.Render, s sessions.Session) { 50 | if u := s.Get("userID"); u != nil { 51 | r.JSON(200, map[string]interface{}{"message": "You did something that required a valid token!"}) 52 | return 53 | } 54 | r.JSON(401, nil) 55 | }) 56 | 57 | m.Run() 58 | } 59 | -------------------------------------------------------------------------------- /examples/angular/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Martini CSRF 4 | 5 | 6 | 7 | 8 | 9 |

You're now at a protected page. Angular is going to do a protected action that requires a csrf token

10 | 11 |

Protected Result

12 |

{{result}}

13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/angular/templates/login.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Martini CSRF 4 | 5 | 6 |

This form simulates a login and generates a new session id. You can put whatever you want in these inputs, 7 | it doesn't matter.

8 |
9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/server_rendered/server.go: -------------------------------------------------------------------------------- 1 | // Simple example using Martini Render HTML templates. 2 | // Passes the csrf.Token to the template that then 3 | // places it in a hidden _csrf input. 4 | 5 | package main 6 | 7 | import ( 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | 12 | "github.com/go-martini/martini" 13 | "github.com/martini-contrib/csrf" 14 | "github.com/martini-contrib/render" 15 | "github.com/martini-contrib/sessions" 16 | ) 17 | 18 | func main() { 19 | m := martini.Classic() 20 | store := sessions.NewCookieStore([]byte("secret123")) 21 | m.Use(render.Renderer()) 22 | m.Use(sessions.Sessions("my_session", store)) 23 | m.Use(csrf.Generate(&csrf.Options{ 24 | Secret: "token123", 25 | SessionKey: "userID", 26 | ErrorFunc: func(w http.ResponseWriter) { 27 | buf, _ := ioutil.ReadFile("templates/error.html") 28 | w.Header().Set("Content-Type", "text/html; charset=utf-8") 29 | w.WriteHeader(422) 30 | fmt.Fprintln(w, string(buf)) 31 | }, 32 | })) 33 | 34 | m.Get("/", func(s sessions.Session, r render.Render, x csrf.CSRF) { 35 | if s.Get("userID") == nil { 36 | r.Redirect("/login", 302) 37 | return 38 | } 39 | r.HTML(200, "index", x.GetToken()) 40 | }) 41 | 42 | m.Get("/login", func(r render.Render) { 43 | r.HTML(200, "login", nil) 44 | }) 45 | 46 | m.Post("/login", func(s sessions.Session, r render.Render) { 47 | s.Set("userID", "123456") 48 | r.Redirect("/") 49 | }) 50 | 51 | m.Post("/protected", csrf.Validate, func(s sessions.Session, r render.Render) { 52 | if s.Get("userID") != nil { 53 | r.HTML(200, "result", "You submitted a valid token") 54 | return 55 | } 56 | r.Redirect("/login", 401) 57 | }) 58 | 59 | m.Get("/error", func(r render.Render) { 60 | r.HTML(200, "custom_error", nil) 61 | }) 62 | 63 | m.Run() 64 | 65 | } 66 | -------------------------------------------------------------------------------- /examples/server_rendered/templates/custom_error.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Martini CSRF 4 | 5 | 6 |

This form simulates a custom error response. It doesn't matter what you enter in these inputs. It will always return an error.

7 |
8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/server_rendered/templates/error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oh no — Martini CSRF 5 | 14 | 15 | 16 |
17 |

Martini CSRF

18 |

422 error‽

19 |

Your browser did something unexpected.
Please contact us if the problem persists.

20 |
21 | 22 | 23 | -------------------------------------------------------------------------------- /examples/server_rendered/templates/index.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Martini CSRF 4 | 5 | 6 |

The form contains a hidden _csrf form value that will be submitted with this form.

7 |
8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/server_rendered/templates/login.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Martini CSRF 4 | 5 | 6 |

This form simulates a login and generates a new session id. You can put whatever you want in these inputs, 7 | it doesn't matter.

8 |
9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/server_rendered/templates/result.tmpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | Martini CSRF 4 | 5 | 6 |

{{.}}

7 | 8 | 9 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: wercker/golang@1.1.1 2 | --------------------------------------------------------------------------------