From 2dc211960426a6473a666b24c8754a8ca9a62c35 Mon Sep 17 00:00:00 2001 From: hackerman <3372410+aeneasr@users.noreply.github.com> Date: Wed, 3 Nov 2021 13:41:30 +0100 Subject: [PATCH] feat: add disable (#6) * feat: add disable * ci: run on ubuntu latest --- .github/workflows/test.yml | 2 +- contract.go | 33 ++++++++++++++++++++++++++++ disable.go | 39 +++++++++++++++++++++++++++++++++ disable_test.go | 45 ++++++++++++++++++++++++++++++++++++++ handler.go | 11 ++++++++++ 5 files changed, 129 insertions(+), 1 deletion(-) create mode 100644 contract.go create mode 100644 disable.go create mode 100644 disable_test.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c5d30e4..f7058d2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,7 +2,7 @@ on: push: jobs: build: - runs-on: ubuntu-16.04 + runs-on: ubuntu-latest strategy: matrix: go: [ '1.15', '1.14', '1.13' ] diff --git a/contract.go b/contract.go new file mode 100644 index 0000000..3420863 --- /dev/null +++ b/contract.go @@ -0,0 +1,33 @@ +package nosurf + +import "net/http" + +type Handler interface { + http.Handler + // RegenerateToken regenerates a CSRF token and sets the cookie. + RegenerateToken(w http.ResponseWriter, r *http.Request) string + + // ExemptPath will not require CSRF validation but will still set the + // cookie if it has not yet been set. + ExemptPath(string) + + // IgnorePath will not require CSRF validation and also not set the CSRF + // cookie, but it will set the CSRF token (if available) in the request context. + IgnorePath(string) + + // IgnoreGlob behaves similar to IgnorePath but allows defining a glob. + IgnoreGlob(string) + + // IgnoreGlobs behaves similar to IgnorePath but allows defining globs. + IgnoreGlobs(...string) + + // DisablePath will not require CSRF validation and also not set the CSRF + // cookie, and it will also not set the CSRF token in the request context. + DisablePath(string) + + // DisableGlob behaves similar to DisablePath but allows defining a glob. + DisableGlob(string) + + // DisableGlobs behaves similar to DisablePath but allows defining globs. + DisableGlobs(...string) +} diff --git a/disable.go b/disable.go new file mode 100644 index 0000000..e0af39d --- /dev/null +++ b/disable.go @@ -0,0 +1,39 @@ +package nosurf + +import ( + "net/http" + pathModule "path" +) + +// Disables the CSRF middleware for an exact path +// With this you should take note that Go's paths +// include a leading slash. +func (h *CSRFHandler) DisablePath(path string) { + h.disablePaths = append(h.disablePaths, path) +} + +// Checks if the given request disables this middleware +func (h *CSRFHandler) IsDisabled(r *http.Request) bool { + path := r.URL.Path + if sContains(h.disablePaths, path) { + return true + } + + // then the globs + for _, glob := range h.disableGlobs { + matched, err := pathModule.Match(glob, path) + if matched && err == nil { + return true + } + } + + return false +} + +func (h *CSRFHandler) DisableGlob(pattern string) { + h.disableGlobs = append(h.disableGlobs, pattern) +} + +func (h *CSRFHandler) DisableGlobs(patterns ...string) { + h.disableGlobs = append(h.disableGlobs, patterns...) +} diff --git a/disable_test.go b/disable_test.go new file mode 100644 index 0000000..266938e --- /dev/null +++ b/disable_test.go @@ -0,0 +1,45 @@ +package nosurf + +import ( + "net/http" + "testing" +) + +func TestDisablePath(t *testing.T) { + // the handler doesn't matter here, let's use nil + hand := New(nil) + path := "/home" + exempt, _ := http.NewRequest("GET", path, nil) + + hand.DisablePath(path) + if !hand.IsDisabled(exempt) { + t.Errorf("%v is not exempt, but it should be", exempt.URL.Path) + } + + other, _ := http.NewRequest("GET", "/faq", nil) + if hand.IsDisabled(other) { + t.Errorf("%v is exempt, but it shouldn't be", other.URL.Path) + } +} + +func TestDisableGlob(t *testing.T) { + hand := New(nil) + glob := "/nail/*" + + hand.DisableGlob(glob) + + test, _ := http.NewRequest("GET", "/nail/foo", nil) + if !hand.IsDisabled(test) { + t.Errorf("%v should be exempt, but it isn't.", test) + } + + test, _ = http.NewRequest("GET", "/nail/foo/bar", nil) + if hand.IsDisabled(test) { + t.Errorf("%v should not be exempt, but it is.", test) + } + + test, _ = http.NewRequest("GET", "/not-nail/foo", nil) + if hand.IsDisabled(test) { + t.Errorf("%v should not be exempt, but it is.", test) + } +} diff --git a/handler.go b/handler.go index 2a29aec..254029e 100644 --- a/handler.go +++ b/handler.go @@ -60,6 +60,11 @@ type CSRFHandler struct { // ...or a glob (as used by path.Match()). ignoreGlobs []string + // Slices of paths that completely disable this middleware. + disablePaths []string + // ...or a glob (as used by path.Match()). + disableGlobs []string + // All of those will be matched against Request.URL.Path, // So they should take the leading slash into account } @@ -121,6 +126,11 @@ func (h CSRFHandler) getCookieName(w http.ResponseWriter, r *http.Request) strin } func (h *CSRFHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if h.IsDisabled(r) { + h.handleSuccess(w, r) + return + } + r = addNosurfContext(r) defer ctxClear(r) @@ -148,6 +158,7 @@ func (h *CSRFHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } else { ctxSetToken(r, realToken) } + if h.IsIgnored(r) { h.handleSuccess(w, r) return