add CSRF protection

updates #14
This commit is contained in:
Henry 2021-02-16 11:20:38 +01:00
parent c3286fb5da
commit f1f4e9dcb9
9 changed files with 75 additions and 10 deletions

1
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/friendsofgo/errors v0.9.2
github.com/go-kit/kit v0.10.0
github.com/gofrs/uuid v4.0.0+incompatible // indirect
github.com/gorilla/csrf v1.7.0
github.com/gorilla/mux v1.8.0
github.com/gorilla/securecookie v1.1.1
github.com/gorilla/sessions v1.2.1

2
go.sum
View File

@ -153,6 +153,8 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/csrf v1.7.0 h1:mMPjV5/3Zd460xCavIkppUdvnl5fPXMpv2uz2Zyg7/Y=
github.com/gorilla/csrf v1.7.0/go.mod h1:+a/4tCmqhG6/w4oafeAZ9pEa3/NZOWYVbD9fV0FwIQA=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=

View File

@ -9,6 +9,7 @@ import (
"go.mindeco.de/http/render"
refs "go.mindeco.de/ssb-refs"
"github.com/gorilla/csrf"
"github.com/ssb-ngi-pointer/go-ssb-room/admindb"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
)
@ -65,10 +66,11 @@ func (h allowListH) overview(rw http.ResponseWriter, req *http.Request) (interfa
return nil, err
}
return struct {
Entries admindb.ListEntries
Count int
}{lst, len(lst)}, nil
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
"Entries": lst,
"Count": len(lst),
}, nil
}
// TODO: move to render package so that we can decide to not render a page during the controller
@ -89,10 +91,10 @@ func (h allowListH) removeConfirm(rw http.ResponseWriter, req *http.Request) (in
}
return nil, err
}
return struct {
Entry admindb.ListEntry
}{entry}, nil
return map[string]interface{}{
"Entry": entry,
csrf.TemplateTag: csrf.TemplateField(req),
}, nil
}
func (h allowListH) remove(rw http.ResponseWriter, req *http.Request) {

View File

@ -6,6 +6,7 @@ import (
"net/http"
"github.com/go-kit/kit/log/level"
"github.com/gorilla/csrf"
"github.com/gorilla/mux"
"go.mindeco.de/http/auth"
"go.mindeco.de/http/render"
@ -23,7 +24,12 @@ func Handler(m *mux.Router, r *render.Renderer, a *auth.Handler) http.Handler {
m = router.Auth(nil)
}
m.Get(router.AuthFallbackSignInForm).Handler(r.StaticHTML("/auth/fallback_sign_in.tmpl"))
m.Get(router.AuthFallbackSignInForm).Handler(r.HTML("/auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
}, nil
}))
m.Get(router.AuthFallbackSignIn).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
logger := logging.FromContext(req.Context())
level.Info(logger).Log("event", "authorize request")

View File

@ -8,6 +8,7 @@ import (
"net/http"
"os"
"github.com/gorilla/csrf"
"github.com/gorilla/sessions"
"go.mindeco.de/http/auth"
"go.mindeco.de/http/render"
@ -157,6 +158,20 @@ func New(
return nil, fmt.Errorf("web Handler: failed to init fallback auth system: %w", err)
}
// Cross Site Request Forgery prevention middleware
csrfKey, err := web.LoadOrCreateCSRFSecret(repo)
if err != nil {
return nil, err
}
CSRF := csrf.Protect(csrfKey,
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
err := csrf.FailureReason(req)
// TODO: localize error?
r.Error(w, req, http.StatusForbidden, err)
})),
)
// this router is a bit of a qurik
// TODO: explain problem between gorilla/mux named routers and authentication
mainMux := &http.ServeMux{}
@ -181,9 +196,10 @@ func New(
mainMux.Handle("/", m)
// apply middleware
var finalHandler http.Handler = mainMux
finalHandler = logging.InjectHandler(logger)(finalHandler)
finalHandler = CSRF(finalHandler)
if web.Production {
return finalHandler, nil

View File

@ -6,6 +6,7 @@
<form id="confirm" action="{{urlTo "admin:allow-list:remove"}}" method="POST">
{{ .csrfField }}
<input type="hidden" name="id" value={{.Entry.ID}}>
<input type="submit" value="{{i18n "Confirm"}}">
</form>

View File

@ -15,6 +15,7 @@
<h3>{{i18n "AdminAllowListAdd"}}</h3>
<form id="add-entry" action="{{urlTo "admin:allow-list:add"}}" method="POST">
{{ .csrfField }}
<input type="text" name="pub_key">
<input type="submit" value="{{i18n "AdminAllowListAdd"}}">
</form>

View File

@ -6,6 +6,7 @@
<div class="row">
<div class="col-md-12">
<form method="POST" action={{urlTo "auth:fallback:signin"}}>
{{ .csrfField }}
<input type="text" name="user">
<input type="password" name="pass">
<input type="submit">

View File

@ -140,3 +140,38 @@ func LoadOrCreateCookieSecrets(repo repo.Interface) ([]securecookie.Codec, error
sc := securecookie.CodecsFromPairs(pairs...)
return sc, nil
}
const csrfKeyLength = 32
// LoadOrCreateCSRFSecret either loads the bytes from $repo/web/csrf-secret or creates a new file with suitable keys in it
func LoadOrCreateCSRFSecret(repo repo.Interface) ([]byte, error) {
secretPath := repo.GetPath("web", "csrf-secret")
err := os.MkdirAll(filepath.Dir(secretPath), 0700)
if err != nil && !os.IsExist(err) {
return nil, fmt.Errorf("failed to create folder for csrf secret: %w", err)
}
// load the existing data
secret, err := ioutil.ReadFile(secretPath)
if err != nil {
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to load csrf secrets: %w", err)
}
// create a new key, save and return it
freshKey := securecookie.GenerateRandomKey(csrfKeyLength)
err = ioutil.WriteFile(secretPath, freshKey, 0600)
if err != nil {
return nil, err
}
return freshKey, nil
}
if n := len(secret); csrfKeyLength != n {
return nil, fmt.Errorf("expected %d bytes csrf secert but got %d", csrfKeyLength, n)
}
return secret, nil
}