diff --git a/http/config.go b/http/config.go index 0232922e2f272098a011bcd404d8f0b73cc91ce1..6dc3877c9d33cdc0d83648a09f3290f6d971b830 100644 --- a/http/config.go +++ b/http/config.go @@ -3,6 +3,7 @@ package http import ( "net/http" "net/url" + "strings" "sync" cors "github.com/rs/cors" @@ -150,3 +151,34 @@ func allowReferer(r *http.Request, cfg *ServerConfig) bool { return false } + +// allowUserAgent checks the request's user-agent against the list +// of DisallowUserAgents for requests with no origin nor referer set. +func allowUserAgent(r *http.Request, cfg *ServerConfig) bool { + // This check affects POST as we should never get POST requests from a + // browser without Origin or Referer, but we might: + // https://bugzilla.mozilla.org/show_bug.cgi?id=429594. + if r.Method != http.MethodPost { + return true + } + + origin := r.Header.Get("Origin") + referer := r.Referer() + + // If these are set, we leave up to CORS and CSRF checks. + if origin != "" || referer != "" { + return true + } + + // Allow if the user agent does not start with Mozilla... (i.e. curl) + ua := r.Header.Get("User-agent") + if !strings.HasPrefix(ua, "Mozilla") { + return true + } + + // Disallow otherwise. + // + // This means the request probably came from a browser and thus, it + // should have included Origin or referer headers. + return false +} diff --git a/http/errors_test.go b/http/errors_test.go index a320b7d7c16dfc393a282a0d081e4c501843871a..48b8359319ac8c95fc693b3e9bb397bc2c1b1e1d 100644 --- a/http/errors_test.go +++ b/http/errors_test.go @@ -170,3 +170,42 @@ func TestUnhandledMethod(t *testing.T) { } tc.test(t) } + +func TestDisallowedUserAgents(t *testing.T) { + tcs := []httpTestCase{ + { + // Block Mozilla* browsers that do not provide origins. + Method: "POST", + AllowGet: false, + Code: http.StatusForbidden, + ReqHeaders: map[string]string{ + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0", + }, + }, + { + // Do not block on GETs + Method: "GET", + AllowGet: true, + Code: http.StatusOK, + ReqHeaders: map[string]string{ + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0", + }, + }, + { + // Do not block a Mozilla* browser that provides an + // allowed Origin + Method: "POST", + AllowGet: false, + AllowOrigins: []string{"*"}, + Origin: "null", + Code: http.StatusOK, + ReqHeaders: map[string]string{ + "User-Agent": "Mozilla/5.0 (Linux; U; Android 4.1.1; en-gb; Build/KLP) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Safari/534.30", + }, + }, + } + + for _, tc := range tcs { + tc.test(t) + } +} diff --git a/http/handler.go b/http/handler.go index b88e4e63619f6dcaabb549b05d3da80f2502b282..2152ea972367eede467fcee9789173b7c462ac19 100644 --- a/http/handler.go +++ b/http/handler.go @@ -121,7 +121,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - if !allowOrigin(r, h.cfg) || !allowReferer(r, h.cfg) { + if !allowOrigin(r, h.cfg) || !allowReferer(r, h.cfg) || !allowUserAgent(r, h.cfg) { http.Error(w, "403 - Forbidden", http.StatusForbidden) log.Warnf("API blocked request to %s. (possible CSRF)", r.URL) return