Skip to content

Commit cd51b41

Browse files
committed
Fix: disallow POST without Origin nor Referer from specific user agents
Addresses browsers being able to POST without control due to things like https://bugzilla.mozilla.org/show_bug.cgi?id=429594
1 parent f2a4f7b commit cd51b41

File tree

3 files changed

+87
-1
lines changed

3 files changed

+87
-1
lines changed

http/config.go

+50
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package http
33
import (
44
"net/http"
55
"net/url"
6+
"strings"
67
"sync"
78

89
cors "github.com/rs/cors"
@@ -14,6 +15,19 @@ const (
1415
ACACredentials = "Access-Control-Allow-Credentials"
1516
)
1617

18+
// disallowedUserAgents specifies a denylist of user agents that are not
19+
// allowed to perform POST requests if they are not providing Origin
20+
// and/or Referer headers. As mitigation for things like
21+
// https://bugzilla.mozilla.org/show_bug.cgi?id=429594. Defaults to
22+
// Firefox-related things. The matching against the user-agent string
23+
// is made with strings.Contains().
24+
var disallowedUserAgents = []string{
25+
"Firefox",
26+
"Focus",
27+
"Klar",
28+
"FxiOS",
29+
}
30+
1731
type ServerConfig struct {
1832
// APIPath is the prefix of all request paths.
1933
// Example: host:port/api/v0/add. Here the APIPath is /api/v0
@@ -30,6 +44,14 @@ type ServerConfig struct {
3044
// websites to include resources from the API but not _read_ them.
3145
AllowGet bool
3246

47+
// DisallowUserAgents specifies a blacklist of user agents that are not
48+
// allowed to perform POST requests if they are not providing Origin
49+
// and/or Referer headers. As mitigation for things like
50+
// https://bugzilla.mozilla.org/show_bug.cgi?id=429594.
51+
// Defaults to ["Firefox"]. The matching against the user-agent
52+
// string is made with strings.Contains().
53+
DisallowUserAgents []string
54+
3355
// corsOpts is a set of options for CORS headers.
3456
corsOpts *cors.Options
3557

@@ -150,3 +172,31 @@ func allowReferer(r *http.Request, cfg *ServerConfig) bool {
150172

151173
return false
152174
}
175+
176+
// allowUserAgent checks the request's user-agent against the list
177+
// of DisallowUserAgents for requests with no origin nor referer set.
178+
func allowUserAgent(r *http.Request, cfg *ServerConfig) bool {
179+
// This check affects POST as we should never get POST requests from a
180+
// browser without Origin or Referer, but we might:
181+
// https://bugzilla.mozilla.org/show_bug.cgi?id=429594.
182+
if r.Method != http.MethodPost {
183+
return true
184+
}
185+
186+
origin := r.Header.Get("Origin")
187+
referer := r.Referer()
188+
189+
// If these are set, we leave up to CORS and CSRF checks.
190+
if origin != "" || referer != "" {
191+
return true
192+
}
193+
194+
// If not, check that request is not from a blacklisted UA.
195+
ua := r.Header.Get("User-agent")
196+
for _, forbiddenUA := range disallowedUserAgents {
197+
if strings.Contains(ua, forbiddenUA) {
198+
return false
199+
}
200+
}
201+
return true
202+
}

http/errors_test.go

+36
Original file line numberDiff line numberDiff line change
@@ -170,3 +170,39 @@ func TestUnhandledMethod(t *testing.T) {
170170
}
171171
tc.test(t)
172172
}
173+
174+
func TestDisallowedUserAgents(t *testing.T) {
175+
tcs := []httpTestCase{
176+
{
177+
// Block Firefox
178+
Method: "POST",
179+
AllowGet: false,
180+
Code: http.StatusForbidden,
181+
ReqHeaders: map[string]string{
182+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0",
183+
},
184+
},
185+
{
186+
// Do not block on GETs
187+
Method: "GET",
188+
AllowGet: true,
189+
Code: http.StatusOK,
190+
ReqHeaders: map[string]string{
191+
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:10.0) Gecko/20100101 Firefox/10.0",
192+
},
193+
},
194+
{
195+
// Do not block Chrome
196+
Method: "POST",
197+
AllowGet: false,
198+
Code: http.StatusOK,
199+
ReqHeaders: map[string]string{
200+
"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",
201+
},
202+
},
203+
}
204+
205+
for _, tc := range tcs {
206+
tc.test(t)
207+
}
208+
}

http/handler.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
121121
return
122122
}
123123

124-
if !allowOrigin(r, h.cfg) || !allowReferer(r, h.cfg) {
124+
if !allowOrigin(r, h.cfg) || !allowReferer(r, h.cfg) || !allowUserAgent(r, h.cfg) {
125125
http.Error(w, "403 - Forbidden", http.StatusForbidden)
126126
log.Warnf("API blocked request to %s. (possible CSRF)", r.URL)
127127
return

0 commit comments

Comments
 (0)