Fix for csrf (#923)
* Merge remote-tracking branch 'refs/remotes/origin/dev' into fix-for-csrf Fix CSRF protection Seems like it doesn't work anymore... I tried to fix it but couldn't get /api without csrf. So I changed the dependency for another csrf package (nosurf). Behavior: Same as previously. You just have to include the block csrf_token * changing dependency to nosurf
Cette révision appartient à :
Parent
75528da943
révision
38a55e88e9
|
@ -173,11 +173,6 @@
|
|||
"Comment": "v1.1-7-g08b5f42",
|
||||
"Rev": "08b5f424b9271eedf6f9f0ce86cb9396ed337a42"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/csrf",
|
||||
"Comment": "v1.5-4-ge6dca67",
|
||||
"Rev": "e6dca6753d92320b18aaf5d4682691b96f7b4564"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/handlers",
|
||||
"Comment": "v1.2-4-g13d7309",
|
||||
|
@ -207,6 +202,11 @@
|
|||
"ImportPath": "github.com/jinzhu/inflection",
|
||||
"Rev": "1c35d901db3da928c72a72d8458480cc9ade058f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/justinas/nosurf",
|
||||
"Comment": "0.1-68-g8e15682",
|
||||
"Rev": "8e15682772641a1e39c431233e6a9338a32def32"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/lib/pq",
|
||||
"Comment": "go1.0-cutoff-166-g2704adc",
|
||||
|
|
2
main.go
2
main.go
|
@ -32,7 +32,7 @@ func RunServer(conf *config.Config) {
|
|||
// TODO Use config from cli
|
||||
os.Mkdir(router.GPGPublicKeyPath, 700)
|
||||
|
||||
http.Handle("/", router.Router)
|
||||
http.Handle("/", router.CSRFRouter)
|
||||
|
||||
// Set up server,
|
||||
srv := &http.Server{
|
||||
|
|
|
@ -3,16 +3,18 @@ package router
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/NyaaPantsu/nyaa/config"
|
||||
"github.com/NyaaPantsu/nyaa/service/captcha"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/handlers"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/justinas/nosurf"
|
||||
)
|
||||
|
||||
// Router variable for exporting the route configuration
|
||||
var Router *mux.Router
|
||||
|
||||
// CSRFRouter : CSRF protection for Router variable for exporting the route configuration
|
||||
var CSRFRouter *nosurf.CSRFHandler
|
||||
|
||||
func init() {
|
||||
// Static file handlers
|
||||
cssHandler := http.FileServer(http.Dir("./public/css/"))
|
||||
|
@ -86,17 +88,6 @@ func init() {
|
|||
userRoutes.HandleFunc("/{id}/{username}/feed", RSSHandler).Name("feed_user")
|
||||
userRoutes.HandleFunc("/{id}/{username}/feed/{page}", RSSHandler).Name("feed_user_page")
|
||||
|
||||
// Please make EnableSecureCSRF to false when testing locally
|
||||
if config.Conf.EnableSecureCSRF {
|
||||
userRoutes.Handle("/", csrf.Protect(config.CSRFTokenHashKey)(userRoutes))
|
||||
torrentRoutes.Handle("/", csrf.Protect(config.CSRFTokenHashKey)(torrentRoutes))
|
||||
torrentViewRoutes.Handle("/", csrf.Protect(config.CSRFTokenHashKey)(torrentViewRoutes))
|
||||
} else {
|
||||
userRoutes.Handle("/", csrf.Protect(config.CSRFTokenHashKey, csrf.Secure(false))(userRoutes))
|
||||
torrentRoutes.Handle("/", csrf.Protect(config.CSRFTokenHashKey, csrf.Secure(false))(torrentRoutes))
|
||||
torrentViewRoutes.Handle("/", csrf.Protect(config.CSRFTokenHashKey, csrf.Secure(false))(torrentViewRoutes))
|
||||
}
|
||||
|
||||
// We don't need CSRF here
|
||||
api := Router.PathPrefix("/api").Subrouter()
|
||||
api.Handle("", wrapHandler(gzipAPIHandler)).Methods("GET")
|
||||
|
@ -152,4 +143,10 @@ func init() {
|
|||
Router.HandleFunc("/settings", ChangePublicSettingsHandler).Methods("POST").Name("see_languages")
|
||||
|
||||
Router.NotFoundHandler = http.HandlerFunc(NotFoundHandler)
|
||||
|
||||
CSRFRouter = nosurf.New(Router)
|
||||
CSRFRouter.ExemptPath("/api")
|
||||
CSRFRouter.ExemptPath("/mod")
|
||||
CSRFRouter.ExemptPath("/upload")
|
||||
CSRFRouter.ExemptPath("/user/login")
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
|
@ -12,8 +11,8 @@ import (
|
|||
userForms "github.com/NyaaPantsu/nyaa/service/user/form"
|
||||
"github.com/NyaaPantsu/nyaa/util/filelist"
|
||||
"github.com/NyaaPantsu/nyaa/util/publicSettings"
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/justinas/nosurf"
|
||||
)
|
||||
|
||||
/* Each Page should have an object to pass to their own template
|
||||
|
@ -106,7 +105,7 @@ type commonTemplateVariables struct {
|
|||
User *model.User
|
||||
URL *url.URL // for parsing URL in templates
|
||||
Route *mux.Route // for getting current route in templates
|
||||
CsrfField template.HTML
|
||||
CsrfToken string
|
||||
Config *config.Config
|
||||
}
|
||||
|
||||
|
@ -159,7 +158,7 @@ func newCommonVariables(r *http.Request) commonTemplateVariables {
|
|||
User: getUser(r),
|
||||
URL: r.URL,
|
||||
Route: mux.CurrentRoute(r),
|
||||
CsrfField: csrf.TemplateField(r),
|
||||
CsrfToken: nosurf.Token(r),
|
||||
Config: config.Conf,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{{ define "csrf_field"}}
|
||||
{{$.CsrfField}}
|
||||
<input type="hidden" name="csrf_token" value="{{ .CsrfToken }}">
|
||||
{{end}}
|
|
@ -1,22 +0,0 @@
|
|||
language: go
|
||||
sudo: false
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- go: 1.2
|
||||
- go: 1.3
|
||||
- go: 1.4
|
||||
- go: 1.5
|
||||
- go: 1.6
|
||||
- go: tip
|
||||
allow_failures:
|
||||
- go: tip
|
||||
|
||||
install:
|
||||
- # skip
|
||||
|
||||
script:
|
||||
- go get -t -v ./...
|
||||
- diff -u <(echo -n) <(gofmt -d .)
|
||||
- go vet $(go list ./... | grep -v /vendor/)
|
||||
- go test -v -race ./...
|
|
@ -1,27 +0,0 @@
|
|||
Copyright (c) 2015, Matt Silverlock (matt@eatsleeprepeat.net) All rights
|
||||
reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without modification,
|
||||
are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation and/or
|
||||
other materials provided with the distribution.
|
||||
|
||||
3. Neither the name of the copyright holder nor the names of its contributors
|
||||
may be used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@ -1,232 +0,0 @@
|
|||
# gorilla/csrf
|
||||
[![GoDoc](https://godoc.org/github.com/gorilla/csrf?status.svg)](https://godoc.org/github.com/gorilla/csrf) [![Build Status](https://travis-ci.org/gorilla/csrf.svg?branch=master)](https://travis-ci.org/gorilla/csrf) [![Sourcegraph](https://sourcegraph.com/github.com/gorilla/csrf/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/csrf?badge)
|
||||
|
||||
gorilla/csrf is a HTTP middleware library that provides [cross-site request
|
||||
forgery](http://blog.codinghorror.com/preventing-csrf-and-xsrf-attacks/) (CSRF)
|
||||
protection. It includes:
|
||||
|
||||
* The `csrf.Protect` middleware/handler provides CSRF protection on routes
|
||||
attached to a router or a sub-router.
|
||||
* A `csrf.Token` function that provides the token to pass into your response,
|
||||
whether that be a HTML form or a JSON response body.
|
||||
* ... and a `csrf.TemplateField` helper that you can pass into your `html/template`
|
||||
templates to replace a `{{ .csrfField }}` template tag with a hidden input
|
||||
field.
|
||||
|
||||
gorilla/csrf is designed to work with any Go web framework, including:
|
||||
|
||||
* The [Gorilla](http://www.gorillatoolkit.org/) toolkit
|
||||
* Go's built-in [net/http](http://golang.org/pkg/net/http/) package
|
||||
* [Goji](https://goji.io) - see the [tailored fork](https://github.com/goji/csrf)
|
||||
* [Gin](https://github.com/gin-gonic/gin)
|
||||
* [Echo](https://github.com/labstack/echo)
|
||||
* ... and any other router/framework that rallies around Go's `http.Handler` interface.
|
||||
|
||||
gorilla/csrf is also compatible with middleware 'helper' libraries like
|
||||
[Alice](https://github.com/justinas/alice) and [Negroni](https://github.com/codegangsta/negroni).
|
||||
|
||||
## Install
|
||||
|
||||
With a properly configured Go toolchain:
|
||||
```sh
|
||||
go get github.com/gorilla/csrf
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
* [HTML Forms](#html-forms)
|
||||
* [JavaScript Apps](#javascript-applications)
|
||||
* [Google App Engine](#google-app-engine)
|
||||
* [Setting Options](#setting-options)
|
||||
|
||||
gorilla/csrf is easy to use: add the middleware to your router with
|
||||
the below:
|
||||
|
||||
```go
|
||||
CSRF := csrf.Protect([]byte("32-byte-long-auth-key"))
|
||||
http.ListenAndServe(":8000", CSRF(r))
|
||||
```
|
||||
|
||||
...and then collect the token with `csrf.Token(r)` in your handlers before
|
||||
passing it to the template, JSON body or HTTP header (see below).
|
||||
|
||||
Note that the authentication key passed to `csrf.Protect([]byte(key))` should be
|
||||
32-bytes long and persist across application restarts. Generating a random key
|
||||
won't allow you to authenticate existing cookies and will break your CSRF
|
||||
validation.
|
||||
|
||||
gorilla/csrf inspects the HTTP headers (first) and form body (second) on
|
||||
subsequent POST/PUT/PATCH/DELETE/etc. requests for the token.
|
||||
|
||||
### HTML Forms
|
||||
|
||||
Here's the common use-case: HTML forms you want to provide CSRF protection for,
|
||||
in order to protect malicious POST requests being made:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/signup", ShowSignupForm)
|
||||
// All POST requests without a valid token will return HTTP 403 Forbidden.
|
||||
r.HandleFunc("/signup/post", SubmitSignupForm)
|
||||
|
||||
// Add the middleware to your router by wrapping it.
|
||||
http.ListenAndServe(":8000",
|
||||
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
|
||||
// PS: Don't forget to pass csrf.Secure(false) if you're developing locally
|
||||
// over plain HTTP (just don't leave it on in production).
|
||||
}
|
||||
|
||||
func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
|
||||
// signup_form.tmpl just needs a {{ .csrfField }} template tag for
|
||||
// csrf.TemplateField to inject the CSRF token into. Easy!
|
||||
t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{
|
||||
csrf.TemplateTag: csrf.TemplateField(r),
|
||||
})
|
||||
// We could also retrieve the token directly from csrf.Token(r) and
|
||||
// set it in the request header - w.Header.Set("X-CSRF-Token", token)
|
||||
// This is useful if you're sending JSON to clients or a front-end JavaScript
|
||||
// framework.
|
||||
}
|
||||
|
||||
func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
|
||||
// We can trust that requests making it this far have satisfied
|
||||
// our CSRF protection requirements.
|
||||
}
|
||||
```
|
||||
|
||||
Note that the CSRF middleware will (by necessity) consume the request body if the
|
||||
token is passed via POST form values. If you need to consume this in your
|
||||
handler, insert your own middleware earlier in the chain to capture the request
|
||||
body.
|
||||
|
||||
### JavaScript Applications
|
||||
|
||||
This approach is useful if you're using a front-end JavaScript framework like
|
||||
React, Ember or Angular, or are providing a JSON API.
|
||||
|
||||
We'll also look at applying selective CSRF protection using
|
||||
[gorilla/mux's](http://www.gorillatoolkit.org/pkg/mux) sub-routers,
|
||||
as we don't handle any POST/PUT/DELETE requests with our top-level router.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
|
||||
api := r.PathPrefix("/api").Subrouter()
|
||||
api.HandleFunc("/user/{id}", GetUser).Methods("GET")
|
||||
|
||||
http.ListenAndServe(":8000",
|
||||
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
|
||||
}
|
||||
|
||||
func GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
// Authenticate the request, get the id from the route params,
|
||||
// and fetch the user from the DB, etc.
|
||||
|
||||
// Get the token and pass it in the CSRF header. Our JSON-speaking client
|
||||
// or JavaScript framework can now read the header and return the token in
|
||||
// in its own "X-CSRF-Token" request header on the subsequent POST.
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
b, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
```
|
||||
|
||||
### Google App Engine
|
||||
|
||||
If you're using [Google App
|
||||
Engine](https://cloud.google.com/appengine/docs/go/how-requests-are-handled#Go_Requests_and_HTTP),
|
||||
which doesn't allow you to hook into the default `http.ServeMux` directly,
|
||||
you can still use gorilla/csrf (and gorilla/mux):
|
||||
|
||||
```go
|
||||
package app
|
||||
|
||||
// Remember: appengine has its own package main
|
||||
func init() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/", IndexHandler)
|
||||
// ...
|
||||
|
||||
// We pass our CSRF-protected router to the DefaultServeMux
|
||||
http.Handle("/", csrf.Protect([]byte(your-key))(r))
|
||||
}
|
||||
```
|
||||
|
||||
### Setting Options
|
||||
|
||||
What about providing your own error handler and changing the HTTP header the
|
||||
package inspects on requests? (i.e. an existing API you're porting to Go). Well,
|
||||
gorilla/csrf provides options for changing these as you see fit:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
CSRF := csrf.Protect(
|
||||
[]byte("a-32-byte-long-key-goes-here"),
|
||||
csrf.RequestHeader("Authenticity-Token"),
|
||||
csrf.FieldName("authenticity_token"),
|
||||
csrf.ErrorHandler(http.HandlerFunc(serverError(403))),
|
||||
)
|
||||
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/signup", GetSignupForm)
|
||||
r.HandleFunc("/signup/post", PostSignupForm)
|
||||
|
||||
http.ListenAndServe(":8000", CSRF(r))
|
||||
}
|
||||
```
|
||||
|
||||
Not too bad, right?
|
||||
|
||||
If there's something you're confused about or a feature you would like to see
|
||||
added, open an issue.
|
||||
|
||||
## Design Notes
|
||||
|
||||
Getting CSRF protection right is important, so here's some background:
|
||||
|
||||
* This library generates unique-per-request (masked) tokens as a mitigation
|
||||
against the [BREACH attack](http://breachattack.com/).
|
||||
* The 'base' (unmasked) token is stored in the session, which means that
|
||||
multiple browser tabs won't cause a user problems as their per-request token
|
||||
is compared with the base token.
|
||||
* Operates on a "whitelist only" approach where safe (non-mutating) HTTP methods
|
||||
(GET, HEAD, OPTIONS, TRACE) are the *only* methods where token validation is not
|
||||
enforced.
|
||||
* The design is based on the battle-tested
|
||||
[Django](https://docs.djangoproject.com/en/1.8/ref/csrf/) and [Ruby on
|
||||
Rails](http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html)
|
||||
approaches.
|
||||
* Cookies are authenticated and based on the [securecookie](https://github.com/gorilla/securecookie)
|
||||
library. They're also Secure (issued over HTTPS only) and are HttpOnly
|
||||
by default, because sane defaults are important.
|
||||
* Go's `crypto/rand` library is used to generate the 32 byte (256 bit) tokens
|
||||
and the one-time-pad used for masking them.
|
||||
|
||||
This library does not seek to be adventurous.
|
||||
|
||||
## License
|
||||
|
||||
BSD licensed. See the LICENSE file for details.
|
|
@ -1,29 +0,0 @@
|
|||
// +build go1.7
|
||||
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func contextGet(r *http.Request, key string) (interface{}, error) {
|
||||
val := r.Context().Value(key)
|
||||
if val == nil {
|
||||
return nil, errors.Errorf("no value exists in the context for key %q", key)
|
||||
}
|
||||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func contextSave(r *http.Request, key string, val interface{}) *http.Request {
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, key, val)
|
||||
return r.WithContext(ctx)
|
||||
}
|
||||
|
||||
func contextClear(r *http.Request) {
|
||||
// no-op for go1.7+
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
// +build !go1.7
|
||||
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func contextGet(r *http.Request, key string) (interface{}, error) {
|
||||
if val, ok := context.GetOk(r, key); ok {
|
||||
return val, nil
|
||||
}
|
||||
|
||||
return nil, errors.Errorf("no value exists in the context for key %q", key)
|
||||
}
|
||||
|
||||
func contextSave(r *http.Request, key string, val interface{}) *http.Request {
|
||||
context.Set(r, key, val)
|
||||
return r
|
||||
}
|
||||
|
||||
func contextClear(r *http.Request) {
|
||||
context.Clear(r)
|
||||
}
|
|
@ -1,279 +0,0 @@
|
|||
package csrf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
)
|
||||
|
||||
// CSRF token length in bytes.
|
||||
const tokenLength = 32
|
||||
|
||||
// Context/session keys & prefixes
|
||||
const (
|
||||
tokenKey string = "gorilla.csrf.Token"
|
||||
formKey string = "gorilla.csrf.Form"
|
||||
errorKey string = "gorilla.csrf.Error"
|
||||
skipCheckKey string = "gorilla.csrf.Skip"
|
||||
cookieName string = "_gorilla_csrf"
|
||||
errorPrefix string = "gorilla/csrf: "
|
||||
)
|
||||
|
||||
var (
|
||||
// The name value used in form fields.
|
||||
fieldName = tokenKey
|
||||
// defaultAge sets the default MaxAge for cookies.
|
||||
defaultAge = 3600 * 12
|
||||
// The default HTTP request header to inspect
|
||||
headerName = "X-CSRF-Token"
|
||||
// Idempotent (safe) methods as defined by RFC7231 section 4.2.2.
|
||||
safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"}
|
||||
)
|
||||
|
||||
// TemplateTag provides a default template tag - e.g. {{ .csrfField }} - for use
|
||||
// with the TemplateField function.
|
||||
var TemplateTag = "csrfField"
|
||||
|
||||
var (
|
||||
// ErrNoReferer is returned when a HTTPS request provides an empty Referer
|
||||
// header.
|
||||
ErrNoReferer = errors.New("referer not supplied")
|
||||
// ErrBadReferer is returned when the scheme & host in the URL do not match
|
||||
// the supplied Referer header.
|
||||
ErrBadReferer = errors.New("referer invalid")
|
||||
// ErrNoToken is returned if no CSRF token is supplied in the request.
|
||||
ErrNoToken = errors.New("CSRF token not found in request")
|
||||
// ErrBadToken is returned if the CSRF token in the request does not match
|
||||
// the token in the session, or is otherwise malformed.
|
||||
ErrBadToken = errors.New("CSRF token invalid")
|
||||
)
|
||||
|
||||
type csrf struct {
|
||||
h http.Handler
|
||||
sc *securecookie.SecureCookie
|
||||
st store
|
||||
opts options
|
||||
}
|
||||
|
||||
// options contains the optional settings for the CSRF middleware.
|
||||
type options struct {
|
||||
MaxAge int
|
||||
Domain string
|
||||
Path string
|
||||
// Note that the function and field names match the case of the associated
|
||||
// http.Cookie field instead of the "correct" HTTPOnly name that golint suggests.
|
||||
HttpOnly bool
|
||||
Secure bool
|
||||
RequestHeader string
|
||||
FieldName string
|
||||
ErrorHandler http.Handler
|
||||
CookieName string
|
||||
}
|
||||
|
||||
// Protect is HTTP middleware that provides Cross-Site Request Forgery
|
||||
// protection.
|
||||
//
|
||||
// It securely generates a masked (unique-per-request) token that
|
||||
// can be embedded in the HTTP response (e.g. form field or HTTP header).
|
||||
// The original (unmasked) token is stored in the session, which is inaccessible
|
||||
// by an attacker (provided you are using HTTPS). Subsequent requests are
|
||||
// expected to include this token, which is compared against the session token.
|
||||
// Requests that do not provide a matching token are served with a HTTP 403
|
||||
// 'Forbidden' error response.
|
||||
//
|
||||
// Example:
|
||||
// package main
|
||||
//
|
||||
// import (
|
||||
// "html/template"
|
||||
//
|
||||
// "github.com/gorilla/csrf"
|
||||
// "github.com/gorilla/mux"
|
||||
// )
|
||||
//
|
||||
// var t = template.Must(template.New("signup_form.tmpl").Parse(form))
|
||||
//
|
||||
// func main() {
|
||||
// r := mux.NewRouter()
|
||||
//
|
||||
// r.HandleFunc("/signup", GetSignupForm)
|
||||
// // POST requests without a valid token will return a HTTP 403 Forbidden.
|
||||
// r.HandleFunc("/signup/post", PostSignupForm)
|
||||
//
|
||||
// // Add the middleware to your router.
|
||||
// http.ListenAndServe(":8000",
|
||||
// // Note that the authentication key provided should be 32 bytes
|
||||
// // long and persist across application restarts.
|
||||
// csrf.Protect([]byte("32-byte-long-auth-key"))(r))
|
||||
// }
|
||||
//
|
||||
// func GetSignupForm(w http.ResponseWriter, r *http.Request) {
|
||||
// // signup_form.tmpl just needs a {{ .csrfField }} template tag for
|
||||
// // csrf.TemplateField to inject the CSRF token into. Easy!
|
||||
// t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{
|
||||
// csrf.TemplateTag: csrf.TemplateField(r),
|
||||
// })
|
||||
// // We could also retrieve the token directly from csrf.Token(r) and
|
||||
// // set it in the request header - w.Header.Set("X-CSRF-Token", token)
|
||||
// // This is useful if you're sending JSON to clients or a front-end JavaScript
|
||||
// // framework.
|
||||
// }
|
||||
//
|
||||
func Protect(authKey []byte, opts ...Option) func(http.Handler) http.Handler {
|
||||
return func(h http.Handler) http.Handler {
|
||||
cs := parseOptions(h, opts...)
|
||||
|
||||
// Set the defaults if no options have been specified
|
||||
if cs.opts.ErrorHandler == nil {
|
||||
cs.opts.ErrorHandler = http.HandlerFunc(unauthorizedHandler)
|
||||
}
|
||||
|
||||
if cs.opts.MaxAge < 0 {
|
||||
// Default of 12 hours
|
||||
cs.opts.MaxAge = defaultAge
|
||||
}
|
||||
|
||||
if cs.opts.FieldName == "" {
|
||||
cs.opts.FieldName = fieldName
|
||||
}
|
||||
|
||||
if cs.opts.CookieName == "" {
|
||||
cs.opts.CookieName = cookieName
|
||||
}
|
||||
|
||||
if cs.opts.RequestHeader == "" {
|
||||
cs.opts.RequestHeader = headerName
|
||||
}
|
||||
|
||||
// Create an authenticated securecookie instance.
|
||||
if cs.sc == nil {
|
||||
cs.sc = securecookie.New(authKey, nil)
|
||||
// Use JSON serialization (faster than one-off gob encoding)
|
||||
cs.sc.SetSerializer(securecookie.JSONEncoder{})
|
||||
// Set the MaxAge of the underlying securecookie.
|
||||
cs.sc.MaxAge(cs.opts.MaxAge)
|
||||
}
|
||||
|
||||
if cs.st == nil {
|
||||
// Default to the cookieStore
|
||||
cs.st = &cookieStore{
|
||||
name: cs.opts.CookieName,
|
||||
maxAge: cs.opts.MaxAge,
|
||||
secure: cs.opts.Secure,
|
||||
httpOnly: cs.opts.HttpOnly,
|
||||
path: cs.opts.Path,
|
||||
domain: cs.opts.Domain,
|
||||
sc: cs.sc,
|
||||
}
|
||||
}
|
||||
|
||||
return cs
|
||||
}
|
||||
}
|
||||
|
||||
// Implements http.Handler for the csrf type.
|
||||
func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Skip the check if directed to. This should always be a bool.
|
||||
if val, err := contextGet(r, skipCheckKey); err == nil {
|
||||
if skip, ok := val.(bool); ok {
|
||||
if skip {
|
||||
cs.h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve the token from the session.
|
||||
// An error represents either a cookie that failed HMAC validation
|
||||
// or that doesn't exist.
|
||||
realToken, err := cs.st.Get(r)
|
||||
if err != nil || len(realToken) != tokenLength {
|
||||
// If there was an error retrieving the token, the token doesn't exist
|
||||
// yet, or it's the wrong length, generate a new token.
|
||||
// Note that the new token will (correctly) fail validation downstream
|
||||
// as it will no longer match the request token.
|
||||
realToken, err = generateRandomBytes(tokenLength)
|
||||
if err != nil {
|
||||
r = envError(r, err)
|
||||
cs.opts.ErrorHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Save the new (real) token in the session store.
|
||||
err = cs.st.Save(realToken, w)
|
||||
if err != nil {
|
||||
r = envError(r, err)
|
||||
cs.opts.ErrorHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Save the masked token to the request context
|
||||
r = contextSave(r, tokenKey, mask(realToken, r))
|
||||
// Save the field name to the request context
|
||||
r = contextSave(r, formKey, cs.opts.FieldName)
|
||||
|
||||
// HTTP methods not defined as idempotent ("safe") under RFC7231 require
|
||||
// inspection.
|
||||
if !contains(safeMethods, r.Method) {
|
||||
// Enforce an origin check for HTTPS connections. As per the Django CSRF
|
||||
// implementation (https://goo.gl/vKA7GE) the Referer header is almost
|
||||
// always present for same-domain HTTP requests.
|
||||
if r.URL.Scheme == "https" {
|
||||
// Fetch the Referer value. Call the error handler if it's empty or
|
||||
// otherwise fails to parse.
|
||||
referer, err := url.Parse(r.Referer())
|
||||
if err != nil || referer.String() == "" {
|
||||
r = envError(r, ErrNoReferer)
|
||||
cs.opts.ErrorHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if sameOrigin(r.URL, referer) == false {
|
||||
r = envError(r, ErrBadReferer)
|
||||
cs.opts.ErrorHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// If the token returned from the session store is nil for non-idempotent
|
||||
// ("unsafe") methods, call the error handler.
|
||||
if realToken == nil {
|
||||
r = envError(r, ErrNoToken)
|
||||
cs.opts.ErrorHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Retrieve the combined token (pad + masked) token and unmask it.
|
||||
requestToken := unmask(cs.requestToken(r))
|
||||
|
||||
// Compare the request token against the real token
|
||||
if !compareTokens(requestToken, realToken) {
|
||||
r = envError(r, ErrBadToken)
|
||||
cs.opts.ErrorHandler.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Set the Vary: Cookie header to protect clients from caching the response.
|
||||
w.Header().Add("Vary", "Cookie")
|
||||
|
||||
// Call the wrapped handler/router on success.
|
||||
cs.h.ServeHTTP(w, r)
|
||||
// Clear the request context after the handler has completed.
|
||||
contextClear(r)
|
||||
}
|
||||
|
||||
// unauthorizedhandler sets a HTTP 403 Forbidden status and writes the
|
||||
// CSRF failure reason to the response.
|
||||
func unauthorizedHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, fmt.Sprintf("%s - %s",
|
||||
http.StatusText(http.StatusForbidden), FailureReason(r)),
|
||||
http.StatusForbidden)
|
||||
return
|
||||
}
|
|
@ -1,165 +0,0 @@
|
|||
/*
|
||||
Package csrf (gorilla/csrf) provides Cross Site Request Forgery (CSRF)
|
||||
prevention middleware for Go web applications & services.
|
||||
|
||||
It includes:
|
||||
|
||||
* The `csrf.Protect` middleware/handler provides CSRF protection on routes
|
||||
attached to a router or a sub-router.
|
||||
* A `csrf.Token` function that provides the token to pass into your response,
|
||||
whether that be a HTML form or a JSON response body.
|
||||
* ... and a `csrf.TemplateField` helper that you can pass into your `html/template`
|
||||
templates to replace a `{{ .csrfField }}` template tag with a hidden input
|
||||
field.
|
||||
|
||||
gorilla/csrf is easy to use: add the middleware to individual handlers with
|
||||
the below:
|
||||
|
||||
CSRF := csrf.Protect([]byte("32-byte-long-auth-key"))
|
||||
http.HandlerFunc("/route", CSRF(YourHandler))
|
||||
|
||||
... and then collect the token with `csrf.Token(r)` before passing it to the
|
||||
template, JSON body or HTTP header (you pick!). gorilla/csrf inspects the form body
|
||||
(first) and HTTP headers (second) on subsequent POST/PUT/PATCH/DELETE/etc. requests
|
||||
for the token.
|
||||
|
||||
Note that the authentication key passed to `csrf.Protect([]byte(key))` should be
|
||||
32-bytes long and persist across application restarts. Generating a random key
|
||||
won't allow you to authenticate existing cookies and will break your CSRF
|
||||
validation.
|
||||
|
||||
Here's the common use-case: HTML forms you want to provide CSRF protection for,
|
||||
in order to protect malicious POST requests being made:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
var form = `
|
||||
<html>
|
||||
<head>
|
||||
<title>Sign Up!</title>
|
||||
</head>
|
||||
<body>
|
||||
<form method="POST" action="/signup/post" accept-charset="UTF-8">
|
||||
<input type="text" name="name">
|
||||
<input type="text" name="email">
|
||||
<!--
|
||||
The default template tag used by the CSRF middleware .
|
||||
This will be replaced with a hidden <input> field containing the
|
||||
masked CSRF token.
|
||||
-->
|
||||
{{ .csrfField }}
|
||||
<input type="submit" value="Sign up!">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
|
||||
var t = template.Must(template.New("signup_form.tmpl").Parse(form))
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
r.HandleFunc("/signup", ShowSignupForm)
|
||||
// All POST requests without a valid token will return HTTP 403 Forbidden.
|
||||
r.HandleFunc("/signup/post", SubmitSignupForm)
|
||||
|
||||
// Add the middleware to your router by wrapping it.
|
||||
http.ListenAndServe(":8000",
|
||||
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
|
||||
// PS: Don't forget to pass csrf.Secure(false) if you're developing locally
|
||||
// over plain HTTP (just don't leave it on in production).
|
||||
}
|
||||
|
||||
func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
|
||||
// signup_form.tmpl just needs a {{ .csrfField }} template tag for
|
||||
// csrf.TemplateField to inject the CSRF token into. Easy!
|
||||
t.ExecuteTemplate(w, "signup_form.tmpl", map[string]interface{}{
|
||||
csrf.TemplateTag: csrf.TemplateField(r),
|
||||
})
|
||||
}
|
||||
|
||||
func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
|
||||
// We can trust that requests making it this far have satisfied
|
||||
// our CSRF protection requirements.
|
||||
fmt.Fprintf(w, "%v\n", r.PostForm)
|
||||
}
|
||||
|
||||
Note that the CSRF middleware will (by necessity) consume the request body if the
|
||||
token is passed via POST form values. If you need to consume this in your
|
||||
handler, insert your own middleware earlier in the chain to capture the request
|
||||
body.
|
||||
|
||||
You can also send the CSRF token in the response header. This approach is useful
|
||||
if you're using a front-end JavaScript framework like Ember or Angular, or are
|
||||
providing a JSON API:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/gorilla/csrf"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
func main() {
|
||||
r := mux.NewRouter()
|
||||
|
||||
api := r.PathPrefix("/api").Subrouter()
|
||||
api.HandleFunc("/user/:id", GetUser).Methods("GET")
|
||||
|
||||
http.ListenAndServe(":8000",
|
||||
csrf.Protect([]byte("32-byte-long-auth-key"))(r))
|
||||
}
|
||||
|
||||
func GetUser(w http.ResponseWriter, r *http.Request) {
|
||||
// Authenticate the request, get the id from the route params,
|
||||
// and fetch the user from the DB, etc.
|
||||
|
||||
// Get the token and pass it in the CSRF header. Our JSON-speaking client
|
||||
// or JavaScript framework can now read the header and return the token in
|
||||
// in its own "X-CSRF-Token" request header on the subsequent POST.
|
||||
w.Header().Set("X-CSRF-Token", csrf.Token(r))
|
||||
b, err := json.Marshal(user)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Write(b)
|
||||
}
|
||||
|
||||
If you're writing a client that's supposed to mimic browser behavior, make sure to
|
||||
send back the CSRF cookie (the default name is _gorilla_csrf, but this can be changed
|
||||
with the CookieName Option) along with either the X-CSRF-Token header or the gorilla.csrf.Token form field.
|
||||
|
||||
In addition: getting CSRF protection right is important, so here's some background:
|
||||
|
||||
* This library generates unique-per-request (masked) tokens as a mitigation
|
||||
against the [BREACH attack](http://breachattack.com/).
|
||||
* The 'base' (unmasked) token is stored in the session, which means that
|
||||
multiple browser tabs won't cause a user problems as their per-request token
|
||||
is compared with the base token.
|
||||
* Operates on a "whitelist only" approach where safe (non-mutating) HTTP methods
|
||||
(GET, HEAD, OPTIONS, TRACE) are the *only* methods where token validation is not
|
||||
enforced.
|
||||
* The design is based on the battle-tested
|
||||
[Django](https://docs.djangoproject.com/en/1.8/ref/csrf/) and [Ruby on
|
||||
Rails](http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection.html)
|
||||
approaches.
|
||||
* Cookies are authenticated and based on the [securecookie](https://github.com/gorilla/securecookie)
|
||||
library. They're also Secure (issued over HTTPS only) and are HttpOnly
|
||||
by default, because sane defaults are important.
|
||||
* Go's `crypto/rand` library is used to generate the 32 byte (256 bit) tokens
|
||||
and the one-time-pad used for masking them.
|
||||
|
||||
This library does not seek to be adventurous.
|
||||
|
||||
*/
|
||||
package csrf
|
|
@ -1,203 +0,0 @@
|
|||
package csrf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// Token returns a masked CSRF token ready for passing into HTML template or
|
||||
// a JSON response body. An empty token will be returned if the middleware
|
||||
// has not been applied (which will fail subsequent validation).
|
||||
func Token(r *http.Request) string {
|
||||
if val, err := contextGet(r, tokenKey); err == nil {
|
||||
if maskedToken, ok := val.(string); ok {
|
||||
return maskedToken
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// FailureReason makes CSRF validation errors available in the request context.
|
||||
// This is useful when you want to log the cause of the error or report it to
|
||||
// client.
|
||||
func FailureReason(r *http.Request) error {
|
||||
if val, err := contextGet(r, errorKey); err == nil {
|
||||
if err, ok := val.(error); ok {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnsafeSkipCheck will skip the CSRF check for any requests. This must be
|
||||
// called before the CSRF middleware.
|
||||
//
|
||||
// Note: You should not set this without otherwise securing the request from
|
||||
// CSRF attacks. The primary use-case for this function is to turn off CSRF
|
||||
// checks for non-browser clients using authorization tokens against your API.
|
||||
func UnsafeSkipCheck(r *http.Request) *http.Request {
|
||||
return contextSave(r, skipCheckKey, true)
|
||||
}
|
||||
|
||||
// TemplateField is a template helper for html/template that provides an <input> field
|
||||
// populated with a CSRF token.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// // The following tag in our form.tmpl template:
|
||||
// {{ .csrfField }}
|
||||
//
|
||||
// // ... becomes:
|
||||
// <input type="hidden" name="gorilla.csrf.Token" value="<token>">
|
||||
//
|
||||
func TemplateField(r *http.Request) template.HTML {
|
||||
if name, err := contextGet(r, formKey); err == nil {
|
||||
fragment := fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`,
|
||||
name, Token(r))
|
||||
|
||||
return template.HTML(fragment)
|
||||
}
|
||||
|
||||
return template.HTML("")
|
||||
}
|
||||
|
||||
// mask returns a unique-per-request token to mitigate the BREACH attack
|
||||
// as per http://breachattack.com/#mitigations
|
||||
//
|
||||
// The token is generated by XOR'ing a one-time-pad and the base (session) CSRF
|
||||
// token and returning them together as a 64-byte slice. This effectively
|
||||
// randomises the token on a per-request basis without breaking multiple browser
|
||||
// tabs/windows.
|
||||
func mask(realToken []byte, r *http.Request) string {
|
||||
otp, err := generateRandomBytes(tokenLength)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// XOR the OTP with the real token to generate a masked token. Append the
|
||||
// OTP to the front of the masked token to allow unmasking in the subsequent
|
||||
// request.
|
||||
return base64.StdEncoding.EncodeToString(append(otp, xorToken(otp, realToken)...))
|
||||
}
|
||||
|
||||
// unmask splits the issued token (one-time-pad + masked token) and returns the
|
||||
// unmasked request token for comparison.
|
||||
func unmask(issued []byte) []byte {
|
||||
// Issued tokens are always masked and combined with the pad.
|
||||
if len(issued) != tokenLength*2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We now know the length of the byte slice.
|
||||
otp := issued[tokenLength:]
|
||||
masked := issued[:tokenLength]
|
||||
|
||||
// Unmask the token by XOR'ing it against the OTP used to mask it.
|
||||
return xorToken(otp, masked)
|
||||
}
|
||||
|
||||
// requestToken returns the issued token (pad + masked token) from the HTTP POST
|
||||
// body or HTTP header. It will return nil if the token fails to decode.
|
||||
func (cs *csrf) requestToken(r *http.Request) []byte {
|
||||
// 1. Check the HTTP header first.
|
||||
issued := r.Header.Get(cs.opts.RequestHeader)
|
||||
|
||||
// 2. Fall back to the POST (form) value.
|
||||
if issued == "" {
|
||||
issued = r.PostFormValue(cs.opts.FieldName)
|
||||
}
|
||||
|
||||
// 3. Finally, fall back to the multipart form (if set).
|
||||
if issued == "" && r.MultipartForm != nil {
|
||||
vals := r.MultipartForm.Value[cs.opts.FieldName]
|
||||
|
||||
if len(vals) > 0 {
|
||||
issued = vals[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Decode the "issued" (pad + masked) token sent in the request. Return a
|
||||
// nil byte slice on a decoding error (this will fail upstream).
|
||||
decoded, err := base64.StdEncoding.DecodeString(issued)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return decoded
|
||||
}
|
||||
|
||||
// generateRandomBytes returns securely generated random bytes.
|
||||
// It will return an error if the system's secure random number generator
|
||||
// fails to function correctly.
|
||||
func generateRandomBytes(n int) ([]byte, error) {
|
||||
b := make([]byte, n)
|
||||
_, err := rand.Read(b)
|
||||
// err == nil only if len(b) == n
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b, nil
|
||||
|
||||
}
|
||||
|
||||
// sameOrigin returns true if URLs a and b share the same origin. The same
|
||||
// origin is defined as host (which includes the port) and scheme.
|
||||
func sameOrigin(a, b *url.URL) bool {
|
||||
return (a.Scheme == b.Scheme && a.Host == b.Host)
|
||||
}
|
||||
|
||||
// compare securely (constant-time) compares the unmasked token from the request
|
||||
// against the real token from the session.
|
||||
func compareTokens(a, b []byte) bool {
|
||||
// This is required as subtle.ConstantTimeCompare does not check for equal
|
||||
// lengths in Go versions prior to 1.3.
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
|
||||
return subtle.ConstantTimeCompare(a, b) == 1
|
||||
}
|
||||
|
||||
// xorToken XORs tokens ([]byte) to provide unique-per-request CSRF tokens. It
|
||||
// will return a masked token if the base token is XOR'ed with a one-time-pad.
|
||||
// An unmasked token will be returned if a masked token is XOR'ed with the
|
||||
// one-time-pad used to mask it.
|
||||
func xorToken(a, b []byte) []byte {
|
||||
n := len(a)
|
||||
if len(b) < n {
|
||||
n = len(b)
|
||||
}
|
||||
|
||||
res := make([]byte, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
res[i] = a[i] ^ b[i]
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// contains is a helper function to check if a string exists in a slice - e.g.
|
||||
// whether a HTTP method exists in a list of safe methods.
|
||||
func contains(vals []string, s string) bool {
|
||||
for _, v := range vals {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// envError stores a CSRF error in the request context.
|
||||
func envError(r *http.Request, err error) *http.Request {
|
||||
return contextSave(r, errorKey, err)
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package csrf
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Option describes a functional option for configuring the CSRF handler.
|
||||
type Option func(*csrf)
|
||||
|
||||
// MaxAge sets the maximum age (in seconds) of a CSRF token's underlying cookie.
|
||||
// Defaults to 12 hours.
|
||||
func MaxAge(age int) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.opts.MaxAge = age
|
||||
}
|
||||
}
|
||||
|
||||
// Domain sets the cookie domain. Defaults to the current domain of the request
|
||||
// only (recommended).
|
||||
//
|
||||
// This should be a hostname and not a URL. If set, the domain is treated as
|
||||
// being prefixed with a '.' - e.g. "example.com" becomes ".example.com" and
|
||||
// matches "www.example.com" and "secure.example.com".
|
||||
func Domain(domain string) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.opts.Domain = domain
|
||||
}
|
||||
}
|
||||
|
||||
// Path sets the cookie path. Defaults to the path the cookie was issued from
|
||||
// (recommended).
|
||||
//
|
||||
// This instructs clients to only respond with cookie for that path and its
|
||||
// subpaths - i.e. a cookie issued from "/register" would be included in requests
|
||||
// to "/register/step2" and "/register/submit".
|
||||
func Path(p string) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.opts.Path = p
|
||||
}
|
||||
}
|
||||
|
||||
// Secure sets the 'Secure' flag on the cookie. Defaults to true (recommended).
|
||||
// Set this to 'false' in your development environment otherwise the cookie won't
|
||||
// be sent over an insecure channel. Setting this via the presence of a 'DEV'
|
||||
// environmental variable is a good way of making sure this won't make it to a
|
||||
// production environment.
|
||||
func Secure(s bool) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.opts.Secure = s
|
||||
}
|
||||
}
|
||||
|
||||
// HttpOnly sets the 'HttpOnly' flag on the cookie. Defaults to true (recommended).
|
||||
func HttpOnly(h bool) Option {
|
||||
return func(cs *csrf) {
|
||||
// Note that the function and field names match the case of the
|
||||
// related http.Cookie field instead of the "correct" HTTPOnly name
|
||||
// that golint suggests.
|
||||
cs.opts.HttpOnly = h
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorHandler allows you to change the handler called when CSRF request
|
||||
// processing encounters an invalid token or request. A typical use would be to
|
||||
// provide a handler that returns a static HTML file with a HTTP 403 status. By
|
||||
// default a HTTP 403 status and a plain text CSRF failure reason are served.
|
||||
//
|
||||
// Note that a custom error handler can also access the csrf.FailureReason(r)
|
||||
// function to retrieve the CSRF validation reason from the request context.
|
||||
func ErrorHandler(h http.Handler) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.opts.ErrorHandler = h
|
||||
}
|
||||
}
|
||||
|
||||
// RequestHeader allows you to change the request header the CSRF middleware
|
||||
// inspects. The default is X-CSRF-Token.
|
||||
func RequestHeader(header string) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.opts.RequestHeader = header
|
||||
}
|
||||
}
|
||||
|
||||
// FieldName allows you to change the name attribute of the hidden <input> field
|
||||
// inspected by this package. The default is 'gorilla.csrf.Token'.
|
||||
func FieldName(name string) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.opts.FieldName = name
|
||||
}
|
||||
}
|
||||
|
||||
// CookieName changes the name of the CSRF cookie issued to clients.
|
||||
//
|
||||
// Note that cookie names should not contain whitespace, commas, semicolons,
|
||||
// backslashes or control characters as per RFC6265.
|
||||
func CookieName(name string) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.opts.CookieName = name
|
||||
}
|
||||
}
|
||||
|
||||
// setStore sets the store used by the CSRF middleware.
|
||||
// Note: this is private (for now) to allow for internal API changes.
|
||||
func setStore(s store) Option {
|
||||
return func(cs *csrf) {
|
||||
cs.st = s
|
||||
}
|
||||
}
|
||||
|
||||
// parseOptions parses the supplied options functions and returns a configured
|
||||
// csrf handler.
|
||||
func parseOptions(h http.Handler, opts ...Option) *csrf {
|
||||
// Set the handler to call after processing.
|
||||
cs := &csrf{
|
||||
h: h,
|
||||
}
|
||||
|
||||
// Default to true. See Secure & HttpOnly function comments for rationale.
|
||||
// Set here to allow package users to override the default.
|
||||
cs.opts.Secure = true
|
||||
cs.opts.HttpOnly = true
|
||||
|
||||
// Range over each options function and apply it
|
||||
// to our csrf type to configure it. Options functions are
|
||||
// applied in order, with any conflicting options overriding
|
||||
// earlier calls.
|
||||
for _, option := range opts {
|
||||
option(cs)
|
||||
}
|
||||
|
||||
return cs
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package csrf
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/securecookie"
|
||||
)
|
||||
|
||||
// store represents the session storage used for CSRF tokens.
|
||||
type store interface {
|
||||
// Get returns the real CSRF token from the store.
|
||||
Get(*http.Request) ([]byte, error)
|
||||
// Save stores the real CSRF token in the store and writes a
|
||||
// cookie to the http.ResponseWriter.
|
||||
// For non-cookie stores, the cookie should contain a unique (256 bit) ID
|
||||
// or key that references the token in the backend store.
|
||||
// csrf.GenerateRandomBytes is a helper function for generating secure IDs.
|
||||
Save(token []byte, w http.ResponseWriter) error
|
||||
}
|
||||
|
||||
// cookieStore is a signed cookie session store for CSRF tokens.
|
||||
type cookieStore struct {
|
||||
name string
|
||||
maxAge int
|
||||
secure bool
|
||||
httpOnly bool
|
||||
path string
|
||||
domain string
|
||||
sc *securecookie.SecureCookie
|
||||
}
|
||||
|
||||
// Get retrieves a CSRF token from the session cookie. It returns an empty token
|
||||
// if decoding fails (e.g. HMAC validation fails or the named cookie doesn't exist).
|
||||
func (cs *cookieStore) Get(r *http.Request) ([]byte, error) {
|
||||
// Retrieve the cookie from the request
|
||||
cookie, err := r.Cookie(cs.name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
token := make([]byte, tokenLength)
|
||||
// Decode the HMAC authenticated cookie.
|
||||
err = cs.sc.Decode(cs.name, cookie.Value, &token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return token, nil
|
||||
}
|
||||
|
||||
// Save stores the CSRF token in the session cookie.
|
||||
func (cs *cookieStore) Save(token []byte, w http.ResponseWriter) error {
|
||||
// Generate an encoded cookie value with the CSRF token.
|
||||
encoded, err := cs.sc.Encode(cs.name, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{
|
||||
Name: cs.name,
|
||||
Value: encoded,
|
||||
MaxAge: cs.maxAge,
|
||||
HttpOnly: cs.httpOnly,
|
||||
Secure: cs.secure,
|
||||
Path: cs.path,
|
||||
Domain: cs.domain,
|
||||
}
|
||||
|
||||
// Set the Expires field on the cookie based on the MaxAge
|
||||
// If MaxAge <= 0, we don't set the Expires attribute, making the cookie
|
||||
// session-only.
|
||||
if cs.maxAge > 0 {
|
||||
cookie.Expires = time.Now().Add(
|
||||
time.Duration(cs.maxAge) * time.Second)
|
||||
}
|
||||
|
||||
// Write the authenticated cookie to the response.
|
||||
http.SetCookie(w, cookie)
|
||||
|
||||
return nil
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
|
||||
# Vim stuff
|
||||
*.s[a-w][a-z]
|
||||
*.un~
|
||||
Session.vim
|
||||
.netrwhist
|
||||
*~
|
|
@ -0,0 +1,14 @@
|
|||
language: go
|
||||
|
||||
install:
|
||||
- go get .
|
||||
|
||||
script:
|
||||
- go test -v .
|
||||
|
||||
go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- tip
|
|
@ -0,0 +1,20 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Justinas Stankevicius
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
this software and associated documentation files (the "Software"), to deal in
|
||||
the Software without restriction, including without limitation the rights to
|
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,125 @@
|
|||
# nosurf
|
||||
|
||||
[![Build Status](https://travis-ci.org/justinas/nosurf.svg?branch=master)](https://travis-ci.org/justinas/nosurf)
|
||||
[![GoDoc](http://godoc.org/github.com/justinas/nosurf?status.png)](http://godoc.org/github.com/justinas/nosurf)
|
||||
|
||||
`nosurf` is an HTTP package for Go
|
||||
that helps you prevent Cross-Site Request Forgery attacks.
|
||||
It acts like a middleware and therefore
|
||||
is compatible with basically any Go HTTP application.
|
||||
|
||||
### Why?
|
||||
Even though CSRF is a prominent vulnerability,
|
||||
Go's web-related package infrastructure mostly consists of
|
||||
micro-frameworks that neither do implement CSRF checks,
|
||||
nor should they.
|
||||
|
||||
`nosurf` solves this problem by providing a `CSRFHandler`
|
||||
that wraps your `http.Handler` and checks for CSRF attacks
|
||||
on every non-safe (non-GET/HEAD/OPTIONS/TRACE) method.
|
||||
|
||||
`nosurf` requires Go 1.1 or later.
|
||||
|
||||
### Features
|
||||
|
||||
* Supports any `http.Handler` (frameworks, your own handlers, etc.)
|
||||
and acts like one itself.
|
||||
* Allows exempting specific endpoints from CSRF checks by
|
||||
an exact URL, a glob, or a regular expression.
|
||||
* Allows specifying your own failure handler.
|
||||
Want to present the hacker with an ASCII middle finger
|
||||
instead of the plain old `HTTP 400`? No problem.
|
||||
* Uses masked tokens to mitigate the BREACH attack.
|
||||
* Has no dependencies outside the Go standard library.
|
||||
|
||||
### Example
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/justinas/nosurf"
|
||||
"html/template"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var templateString string = `
|
||||
<!doctype html>
|
||||
<html>
|
||||
<body>
|
||||
{{ if .name }}
|
||||
<p>Your name: {{ .name }}</p>
|
||||
{{ end }}
|
||||
<form action="/" method="POST">
|
||||
<input type="text" name="name">
|
||||
|
||||
<!-- Try removing this or changing its value
|
||||
and see what happens -->
|
||||
<input type="hidden" name="csrf_token" value="{{ .token }}">
|
||||
<input type="submit" value="Send">
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
var templ = template.Must(template.New("t1").Parse(templateString))
|
||||
|
||||
func myFunc(w http.ResponseWriter, r *http.Request) {
|
||||
context := make(map[string]string)
|
||||
context["token"] = nosurf.Token(r)
|
||||
if r.Method == "POST" {
|
||||
context["name"] = r.FormValue("name")
|
||||
}
|
||||
|
||||
templ.Execute(w, context)
|
||||
}
|
||||
|
||||
func main() {
|
||||
myHandler := http.HandlerFunc(myFunc)
|
||||
fmt.Println("Listening on http://127.0.0.1:8000/")
|
||||
http.ListenAndServe(":8000", nosurf.New(myHandler))
|
||||
}
|
||||
```
|
||||
|
||||
### Manual token verification
|
||||
In some cases the CSRF token may be send through a non standard way,
|
||||
e.g. a body or request is a JSON encoded message with one of the fields
|
||||
being a token.
|
||||
|
||||
In such case the handler(path) should be excluded from an automatic
|
||||
verification by using one of the exemption methods:
|
||||
|
||||
```go
|
||||
func (h *CSRFHandler) ExemptFunc(fn func(r *http.Request) bool)
|
||||
func (h *CSRFHandler) ExemptGlob(pattern string)
|
||||
func (h *CSRFHandler) ExemptGlobs(patterns ...string)
|
||||
func (h *CSRFHandler) ExemptPath(path string)
|
||||
func (h *CSRFHandler) ExemptPaths(paths ...string)
|
||||
func (h *CSRFHandler) ExemptRegexp(re interface{})
|
||||
func (h *CSRFHandler) ExemptRegexps(res ...interface{})
|
||||
```
|
||||
|
||||
Later on, the token **must** be verified by manually getting the token from the cookie
|
||||
and providing the token sent in body through: `VerifyToken(tkn, tkn2 string) bool`.
|
||||
|
||||
Example:
|
||||
```go
|
||||
func HandleJson(w http.ResponseWriter, r *http.Request) {
|
||||
d := struct{
|
||||
X,Y int
|
||||
Tkn string
|
||||
}{}
|
||||
json.Unmarshal(ioutil.ReadAll(r.Body), &d)
|
||||
if !nosurf.VerifyToken(Token(r), d.Tkn) {
|
||||
http.Errorf(w, "CSRF token incorrect", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// do smth cool
|
||||
}
|
||||
```
|
||||
|
||||
### Contributing
|
||||
|
||||
0. Find an issue that bugs you / open a new one.
|
||||
1. Discuss.
|
||||
2. Branch off, commit, test.
|
||||
3. Make a pull request / attach the commits to the issue.
|
|
@ -0,0 +1,60 @@
|
|||
// +build go1.7
|
||||
|
||||
package nosurf
|
||||
|
||||
import "net/http"
|
||||
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
nosurfKey ctxKey = iota
|
||||
)
|
||||
|
||||
type csrfContext struct {
|
||||
// The masked, base64 encoded token
|
||||
// That's suitable for use in form fields, etc.
|
||||
token string
|
||||
// reason for the failure of CSRF check
|
||||
reason error
|
||||
}
|
||||
|
||||
// Token takes an HTTP request and returns
|
||||
// the CSRF token for that request
|
||||
// or an empty string if the token does not exist.
|
||||
//
|
||||
// Note that the token won't be available after
|
||||
// CSRFHandler finishes
|
||||
// (that is, in another handler that wraps it,
|
||||
// or after the request has been served)
|
||||
func Token(req *http.Request) string {
|
||||
ctx := req.Context().Value(nosurfKey).(*csrfContext)
|
||||
|
||||
return ctx.token
|
||||
}
|
||||
|
||||
// Reason takes an HTTP request and returns
|
||||
// the reason of failure of the CSRF check for that request
|
||||
//
|
||||
// Note that the same availability restrictions apply for Reason() as for Token().
|
||||
func Reason(req *http.Request) error {
|
||||
ctx := req.Context().Value(nosurfKey).(*csrfContext)
|
||||
|
||||
return ctx.reason
|
||||
}
|
||||
|
||||
func ctxClear(_ *http.Request) {
|
||||
}
|
||||
|
||||
func ctxSetToken(req *http.Request, token []byte) {
|
||||
ctx := req.Context().Value(nosurfKey).(*csrfContext)
|
||||
ctx.token = b64encode(maskToken(token))
|
||||
}
|
||||
|
||||
func ctxSetReason(req *http.Request, reason error) {
|
||||
ctx := req.Context().Value(nosurfKey).(*csrfContext)
|
||||
if ctx.token == "" {
|
||||
panic("Reason should never be set when there's no token in the context yet.")
|
||||
}
|
||||
|
||||
ctx.reason = reason
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
// +build !go1.7
|
||||
|
||||
package nosurf
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// This file implements a context similar to one found
|
||||
// in gorilla/context, but tailored specifically for our use case
|
||||
// and not using gorilla's package just because.
|
||||
|
||||
type csrfContext struct {
|
||||
// The masked, base64 encoded token
|
||||
// That's suitable for use in form fields, etc.
|
||||
token string
|
||||
// reason for the failure of CSRF check
|
||||
reason error
|
||||
}
|
||||
|
||||
var (
|
||||
contextMap = make(map[*http.Request]*csrfContext)
|
||||
cmMutex = new(sync.RWMutex)
|
||||
)
|
||||
|
||||
// Token() takes an HTTP request and returns
|
||||
// the CSRF token for that request
|
||||
// or an empty string if the token does not exist.
|
||||
//
|
||||
// Note that the token won't be available after
|
||||
// CSRFHandler finishes
|
||||
// (that is, in another handler that wraps it,
|
||||
// or after the request has been served)
|
||||
func Token(req *http.Request) string {
|
||||
cmMutex.RLock()
|
||||
defer cmMutex.RUnlock()
|
||||
|
||||
ctx, ok := contextMap[req]
|
||||
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
return ctx.token
|
||||
}
|
||||
|
||||
// Reason() takes an HTTP request and returns
|
||||
// the reason of failure of the CSRF check for that request
|
||||
//
|
||||
// Note that the same availability restrictions apply for Reason() as for Token().
|
||||
func Reason(req *http.Request) error {
|
||||
cmMutex.RLock()
|
||||
defer cmMutex.RUnlock()
|
||||
|
||||
ctx, ok := contextMap[req]
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return ctx.reason
|
||||
}
|
||||
|
||||
// Takes a raw token, masks it with a per-request key,
|
||||
// encodes in base64 and makes it available to the wrapped handler
|
||||
func ctxSetToken(req *http.Request, token []byte) *http.Request {
|
||||
cmMutex.Lock()
|
||||
defer cmMutex.Unlock()
|
||||
|
||||
ctx, ok := contextMap[req]
|
||||
if !ok {
|
||||
ctx = new(csrfContext)
|
||||
contextMap[req] = ctx
|
||||
}
|
||||
|
||||
ctx.token = b64encode(maskToken(token))
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func ctxSetReason(req *http.Request, reason error) *http.Request {
|
||||
cmMutex.Lock()
|
||||
defer cmMutex.Unlock()
|
||||
|
||||
ctx, ok := contextMap[req]
|
||||
if !ok {
|
||||
panic("Reason should never be set when there's no token" +
|
||||
" (context) yet.")
|
||||
}
|
||||
|
||||
ctx.reason = reason
|
||||
return req
|
||||
}
|
||||
|
||||
func ctxClear(req *http.Request) {
|
||||
cmMutex.Lock()
|
||||
defer cmMutex.Unlock()
|
||||
|
||||
delete(contextMap, req)
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
package nosurf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Masks/unmasks the given data *in place*
|
||||
// with the given key
|
||||
// Slices must be of the same length, or oneTimePad will panic
|
||||
func oneTimePad(data, key []byte) {
|
||||
n := len(data)
|
||||
if n != len(key) {
|
||||
panic("Lengths of slices are not equal")
|
||||
}
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
data[i] ^= key[i]
|
||||
}
|
||||
}
|
||||
|
||||
func maskToken(data []byte) []byte {
|
||||
if len(data) != tokenLength {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tokenLength*2 == len(enckey + token)
|
||||
result := make([]byte, 2*tokenLength)
|
||||
// the first half of the result is the OTP
|
||||
// the second half is the masked token itself
|
||||
key := result[:tokenLength]
|
||||
token := result[tokenLength:]
|
||||
copy(token, data)
|
||||
|
||||
// generate the random token
|
||||
if _, err := io.ReadFull(rand.Reader, key); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
oneTimePad(token, key)
|
||||
return result
|
||||
}
|
||||
|
||||
func unmaskToken(data []byte) []byte {
|
||||
if len(data) != tokenLength*2 {
|
||||
return nil
|
||||
}
|
||||
|
||||
key := data[:tokenLength]
|
||||
token := data[tokenLength:]
|
||||
oneTimePad(token, key)
|
||||
|
||||
return token
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
package nosurf
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
pathModule "path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Checks if the given request is exempt from CSRF checks.
|
||||
// It checks the ExemptFunc first, then the exact paths,
|
||||
// then the globs and finally the regexps.
|
||||
func (h *CSRFHandler) IsExempt(r *http.Request) bool {
|
||||
if h.exemptFunc != nil && h.exemptFunc(r) {
|
||||
return true
|
||||
}
|
||||
|
||||
path := r.URL.Path
|
||||
if sContains(h.exemptPaths, path) {
|
||||
return true
|
||||
}
|
||||
|
||||
// then the globs
|
||||
for _, glob := range h.exemptGlobs {
|
||||
matched, err := pathModule.Match(glob, path)
|
||||
if matched && err == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// finally, the regexps
|
||||
for _, re := range h.exemptRegexps {
|
||||
if re.MatchString(path) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Exempts an exact path from CSRF checks
|
||||
// With this (and other Exempt* methods)
|
||||
// you should take note that Go's paths
|
||||
// include a leading slash.
|
||||
func (h *CSRFHandler) ExemptPath(path string) {
|
||||
h.exemptPaths = append(h.exemptPaths, path)
|
||||
}
|
||||
|
||||
// A variadic argument version of ExemptPath()
|
||||
func (h *CSRFHandler) ExemptPaths(paths ...string) {
|
||||
for _, v := range paths {
|
||||
h.ExemptPath(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Exempts URLs that match the specified glob pattern
|
||||
// (as used by filepath.Match()) from CSRF checks
|
||||
//
|
||||
// Note that ExemptGlob() is unable to detect syntax errors,
|
||||
// because it doesn't have a path to check it against
|
||||
// and filepath.Match() doesn't report an error
|
||||
// if the path is empty.
|
||||
// If we find a way to check the syntax, ExemptGlob
|
||||
// MIGHT PANIC on a syntax error in the future.
|
||||
// ALWAYS check your globs for syntax errors.
|
||||
func (h *CSRFHandler) ExemptGlob(pattern string) {
|
||||
h.exemptGlobs = append(h.exemptGlobs, pattern)
|
||||
}
|
||||
|
||||
// A variadic argument version of ExemptGlob()
|
||||
func (h *CSRFHandler) ExemptGlobs(patterns ...string) {
|
||||
for _, v := range patterns {
|
||||
h.ExemptGlob(v)
|
||||
}
|
||||
}
|
||||
|
||||
// Accepts a regular expression string or a compiled *regexp.Regexp
|
||||
// and exempts URLs that match it from CSRF checks.
|
||||
//
|
||||
// If the given argument is neither of the accepted values,
|
||||
// or the given string fails to compile, ExemptRegexp() panics.
|
||||
func (h *CSRFHandler) ExemptRegexp(re interface{}) {
|
||||
var compiled *regexp.Regexp
|
||||
|
||||
switch re.(type) {
|
||||
case string:
|
||||
compiled = regexp.MustCompile(re.(string))
|
||||
case *regexp.Regexp:
|
||||
compiled = re.(*regexp.Regexp)
|
||||
default:
|
||||
err := fmt.Sprintf("%v isn't a valid type for ExemptRegexp()", reflect.TypeOf(re))
|
||||
panic(err)
|
||||
}
|
||||
|
||||
h.exemptRegexps = append(h.exemptRegexps, compiled)
|
||||
}
|
||||
|
||||
// A variadic argument version of ExemptRegexp()
|
||||
func (h *CSRFHandler) ExemptRegexps(res ...interface{}) {
|
||||
for _, v := range res {
|
||||
h.ExemptRegexp(v)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *CSRFHandler) ExemptFunc(fn func(r *http.Request) bool) {
|
||||
h.exemptFunc = fn
|
||||
}
|
|
@ -0,0 +1,220 @@
|
|||
// Package nosurf implements an HTTP handler that
|
||||
// mitigates Cross-Site Request Forgery Attacks.
|
||||
package nosurf
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
const (
|
||||
// the name of CSRF cookie
|
||||
CookieName = "csrf_token"
|
||||
// the name of the form field
|
||||
FormFieldName = "csrf_token"
|
||||
// the name of CSRF header
|
||||
HeaderName = "X-CSRF-Token"
|
||||
// the HTTP status code for the default failure handler
|
||||
FailureCode = 400
|
||||
|
||||
// Max-Age in seconds for the default base cookie. 365 days.
|
||||
MaxAge = 365 * 24 * 60 * 60
|
||||
)
|
||||
|
||||
var safeMethods = []string{"GET", "HEAD", "OPTIONS", "TRACE"}
|
||||
|
||||
// reasons for CSRF check failures
|
||||
var (
|
||||
ErrNoReferer = errors.New("A secure request contained no Referer or its value was malformed")
|
||||
ErrBadReferer = errors.New("A secure request's Referer comes from a different Origin" +
|
||||
" from the request's URL")
|
||||
ErrBadToken = errors.New("The CSRF token in the cookie doesn't match the one" +
|
||||
" received in a form/header.")
|
||||
)
|
||||
|
||||
type CSRFHandler struct {
|
||||
// Handlers that CSRFHandler wraps.
|
||||
successHandler http.Handler
|
||||
failureHandler http.Handler
|
||||
|
||||
// The base cookie that CSRF cookies will be built upon.
|
||||
// This should be a better solution of customizing the options
|
||||
// than a bunch of methods SetCookieExpiration(), etc.
|
||||
baseCookie http.Cookie
|
||||
|
||||
// Slices of paths that are exempt from CSRF checks.
|
||||
// They can be specified by...
|
||||
// ...an exact path,
|
||||
exemptPaths []string
|
||||
// ...a regexp,
|
||||
exemptRegexps []*regexp.Regexp
|
||||
// ...or a glob (as used by path.Match()).
|
||||
exemptGlobs []string
|
||||
// ...or a custom matcher function
|
||||
exemptFunc func(r *http.Request) bool
|
||||
|
||||
// All of those will be matched against Request.URL.Path,
|
||||
// So they should take the leading slash into account
|
||||
}
|
||||
|
||||
func defaultFailureHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "", FailureCode)
|
||||
}
|
||||
|
||||
// Extracts the "sent" token from the request
|
||||
// and returns an unmasked version of it
|
||||
func extractToken(r *http.Request) []byte {
|
||||
var sentToken string
|
||||
|
||||
// Prefer the header over form value
|
||||
sentToken = r.Header.Get(HeaderName)
|
||||
|
||||
// Then POST values
|
||||
if len(sentToken) == 0 {
|
||||
sentToken = r.PostFormValue(FormFieldName)
|
||||
}
|
||||
|
||||
// If all else fails, try a multipart value.
|
||||
// PostFormValue() will already have called ParseMultipartForm()
|
||||
if len(sentToken) == 0 && r.MultipartForm != nil {
|
||||
vals := r.MultipartForm.Value[FormFieldName]
|
||||
if len(vals) != 0 {
|
||||
sentToken = vals[0]
|
||||
}
|
||||
}
|
||||
|
||||
return b64decode(sentToken)
|
||||
}
|
||||
|
||||
// Constructs a new CSRFHandler that calls
|
||||
// the specified handler if the CSRF check succeeds.
|
||||
func New(handler http.Handler) *CSRFHandler {
|
||||
baseCookie := http.Cookie{}
|
||||
baseCookie.MaxAge = MaxAge
|
||||
|
||||
csrf := &CSRFHandler{successHandler: handler,
|
||||
failureHandler: http.HandlerFunc(defaultFailureHandler),
|
||||
baseCookie: baseCookie,
|
||||
}
|
||||
|
||||
return csrf
|
||||
}
|
||||
|
||||
// The same as New(), but has an interface return type.
|
||||
func NewPure(handler http.Handler) http.Handler {
|
||||
return New(handler)
|
||||
}
|
||||
|
||||
func (h *CSRFHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
r = addNosurfContext(r)
|
||||
defer ctxClear(r)
|
||||
w.Header().Add("Vary", "Cookie")
|
||||
|
||||
var realToken []byte
|
||||
|
||||
tokenCookie, err := r.Cookie(CookieName)
|
||||
if err == nil {
|
||||
realToken = b64decode(tokenCookie.Value)
|
||||
}
|
||||
|
||||
// If the length of the real token isn't what it should be,
|
||||
// it has either been tampered with,
|
||||
// or we're migrating onto a new algorithm for generating tokens,
|
||||
// or it hasn't ever been set so far.
|
||||
// In any case of those, we should regenerate it.
|
||||
//
|
||||
// As a consequence, CSRF check will fail when comparing the tokens later on,
|
||||
// so we don't have to fail it just yet.
|
||||
if len(realToken) != tokenLength {
|
||||
h.RegenerateToken(w, r)
|
||||
} else {
|
||||
ctxSetToken(r, realToken)
|
||||
}
|
||||
|
||||
if sContains(safeMethods, r.Method) || h.IsExempt(r) {
|
||||
// short-circuit with a success for safe methods
|
||||
h.handleSuccess(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// if the request is secure, we enforce origin check
|
||||
// for referer to prevent MITM of http->https requests
|
||||
if r.URL.Scheme == "https" {
|
||||
referer, err := url.Parse(r.Header.Get("Referer"))
|
||||
|
||||
// if we can't parse the referer or it's empty,
|
||||
// we assume it's not specified
|
||||
if err != nil || referer.String() == "" {
|
||||
ctxSetReason(r, ErrNoReferer)
|
||||
h.handleFailure(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// if the referer doesn't share origin with the request URL,
|
||||
// we have another error for that
|
||||
if !sameOrigin(referer, r.URL) {
|
||||
ctxSetReason(r, ErrBadReferer)
|
||||
h.handleFailure(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, we check the token itself.
|
||||
sentToken := extractToken(r)
|
||||
|
||||
if !verifyToken(realToken, sentToken) {
|
||||
ctxSetReason(r, ErrBadToken)
|
||||
h.handleFailure(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Everything else passed, handle the success.
|
||||
h.handleSuccess(w, r)
|
||||
}
|
||||
|
||||
// handleSuccess simply calls the successHandler.
|
||||
// Everything else, like setting a token in the context
|
||||
// is taken care of by h.ServeHTTP()
|
||||
func (h *CSRFHandler) handleSuccess(w http.ResponseWriter, r *http.Request) {
|
||||
h.successHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Same applies here: h.ServeHTTP() sets the failure reason, the token,
|
||||
// and only then calls handleFailure()
|
||||
func (h *CSRFHandler) handleFailure(w http.ResponseWriter, r *http.Request) {
|
||||
h.failureHandler.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
// Generates a new token, sets it on the given request and returns it
|
||||
func (h *CSRFHandler) RegenerateToken(w http.ResponseWriter, r *http.Request) string {
|
||||
token := generateToken()
|
||||
h.setTokenCookie(w, r, token)
|
||||
|
||||
return Token(r)
|
||||
}
|
||||
|
||||
func (h *CSRFHandler) setTokenCookie(w http.ResponseWriter, r *http.Request, token []byte) {
|
||||
// ctxSetToken() does the masking for us
|
||||
ctxSetToken(r, token)
|
||||
|
||||
cookie := h.baseCookie
|
||||
cookie.Name = CookieName
|
||||
cookie.Value = b64encode(token)
|
||||
|
||||
http.SetCookie(w, &cookie)
|
||||
|
||||
}
|
||||
|
||||
// Sets the handler to call in case the CSRF check
|
||||
// fails. By default it's defaultFailureHandler.
|
||||
func (h *CSRFHandler) SetFailureHandler(handler http.Handler) {
|
||||
h.failureHandler = handler
|
||||
}
|
||||
|
||||
// Sets the base cookie to use when building a CSRF token cookie
|
||||
// This way you can specify the Domain, Path, HttpOnly, Secure, etc.
|
||||
func (h *CSRFHandler) SetBaseCookie(cookie http.Cookie) {
|
||||
h.baseCookie = cookie
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
// +build go1.7
|
||||
|
||||
package nosurf
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func addNosurfContext(r *http.Request) *http.Request {
|
||||
return r.WithContext(context.WithValue(r.Context(), nosurfKey, &csrfContext{}))
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// +build !go1.7
|
||||
|
||||
package nosurf
|
||||
|
||||
import "net/http"
|
||||
|
||||
func addNosurfContext(r *http.Request) *http.Request {
|
||||
return r
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
package nosurf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenLength = 32
|
||||
)
|
||||
|
||||
/*
|
||||
There are two types of tokens.
|
||||
|
||||
* The unmasked "real" token consists of 32 random bytes.
|
||||
It is stored in a cookie (base64-encoded) and it's the
|
||||
"reference" value that sent tokens get compared to.
|
||||
|
||||
* The masked "sent" token consists of 64 bytes:
|
||||
32 byte key used for one-time pad masking and
|
||||
32 byte "real" token masked with the said key.
|
||||
It is used as a value (base64-encoded as well)
|
||||
in forms and/or headers.
|
||||
|
||||
Upon processing, both tokens are base64-decoded
|
||||
and then treated as 32/64 byte slices.
|
||||
*/
|
||||
|
||||
// A token is generated by returning tokenLength bytes
|
||||
// from crypto/rand
|
||||
func generateToken() []byte {
|
||||
bytes := make([]byte, tokenLength)
|
||||
|
||||
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
func b64encode(data []byte) string {
|
||||
return base64.StdEncoding.EncodeToString(data)
|
||||
}
|
||||
|
||||
func b64decode(data string) []byte {
|
||||
decoded, err := base64.StdEncoding.DecodeString(data)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
// VerifyToken verifies the sent token equals the real one
|
||||
// and returns a bool value indicating if tokens are equal.
|
||||
// Supports masked tokens. realToken comes from Token(r) and
|
||||
// sentToken is token sent unusual way.
|
||||
func VerifyToken(realToken, sentToken string) bool {
|
||||
r := b64decode(realToken)
|
||||
if len(r) == 2*tokenLength {
|
||||
r = unmaskToken(r)
|
||||
}
|
||||
s := b64decode(sentToken)
|
||||
if len(s) == 2*tokenLength {
|
||||
s = unmaskToken(s)
|
||||
}
|
||||
return subtle.ConstantTimeCompare(r, s) == 1
|
||||
}
|
||||
|
||||
func verifyToken(realToken, sentToken []byte) bool {
|
||||
realN := len(realToken)
|
||||
sentN := len(sentToken)
|
||||
|
||||
// sentN == tokenLength means the token is unmasked
|
||||
// sentN == 2*tokenLength means the token is masked.
|
||||
|
||||
if realN == tokenLength && sentN == 2*tokenLength {
|
||||
return verifyMasked(realToken, sentToken)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies the masked token
|
||||
func verifyMasked(realToken, sentToken []byte) bool {
|
||||
sentPlain := unmaskToken(sentToken)
|
||||
return subtle.ConstantTimeCompare(realToken, sentPlain) == 1
|
||||
}
|
||||
|
||||
func checkForPRNG() {
|
||||
// Check that cryptographically secure PRNG is available
|
||||
// In case it's not, panic.
|
||||
buf := make([]byte, 1)
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("crypto/rand is unavailable: Read() failed with %#v", err))
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
checkForPRNG()
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package nosurf
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func sContains(slice []string, s string) bool {
|
||||
// checks if the given slice contains the given string
|
||||
for _, v := range slice {
|
||||
if v == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Checks if the given URLs have the same origin
|
||||
// (that is, they share the host, the port and the scheme)
|
||||
func sameOrigin(u1, u2 *url.URL) bool {
|
||||
// we take pointers, as url.Parse() returns a pointer
|
||||
// and http.Request.URL is a pointer as well
|
||||
|
||||
// Host is either host or host:port
|
||||
return (u1.Scheme == u2.Scheme && u1.Host == u2.Host)
|
||||
}
|
Référencer dans un nouveau ticket