Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions csrf.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,13 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestURL.Host = r.Host
}

// if we have an Origin header, check it against our allowlist
// if we have an Origin header, check it against our allowlist.
// "null" is a special opaque origin (sandboxed iframes, file://,
// privacy-sensitive contexts per RFC 6454 §7.3); it isn't
// comparable against our allowlist, so treat it like an absent
// header and rely on the Referer check below.
origin := r.Header.Get("Origin")
if origin != "" {
if origin != "" && origin != "null" {
parsedOrigin, err := url.Parse(origin)
if err != nil {
r = envError(r, ErrBadOrigin)
Expand All @@ -298,7 +302,7 @@ func (cs *csrf) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// successful HTTP Machine-in-the-Middle attack and uses this to inject
// a form and cause submission to our origin. We strictly disallow
// cleartext HTTP origins and evaluate the domain against an allowlist.
if origin == "" && !isPlaintext {
if (origin == "" || origin == "null") && !isPlaintext {
// Fetch the Referer value. Call the error handler if it's empty or
// otherwise fails to parse.
referer, err := url.Parse(r.Referer())
Expand Down
62 changes: 62 additions & 0 deletions csrf_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,68 @@ func TestProtectScenarios(t *testing.T) {
}
}

// TestNullOriginFallsBackToReferer makes sure a literal "null" Origin
// header doesn't blow up the origin allowlist check (RFC 6454 §7.3),
// and the request is then evaluated by the Referer rules just like a
// missing Origin would be.
func TestNullOriginFallsBackToReferer(t *testing.T) {
tests := []struct {
name string
secureRequest bool
referer string
token func(rr *httptest.ResponseRecorder, r *http.Request, tok string)
want int
}{
{
name: "cleartext POST with null Origin and valid token passes",
secureRequest: false,
token: func(rr *httptest.ResponseRecorder, r *http.Request, tok string) {
setCookie(rr, r)
r.Header.Set("X-CSRF-Token", tok)
},
want: http.StatusOK,
},
{
name: "TLS POST with null Origin and no Referer is rejected (no Referer)",
secureRequest: true,
token: func(rr *httptest.ResponseRecorder, r *http.Request, tok string) {
setCookie(rr, r)
r.Header.Set("X-CSRF-Token", tok)
},
want: http.StatusForbidden,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var token string
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
token = Token(r)
})
mux.HandleFunc("/submit", func(w http.ResponseWriter, r *http.Request) {})
p := Protect(testKey)(mux)

g := createRequest("GET", "/", tt.secureRequest)
gr := httptest.NewRecorder()
p.ServeHTTP(gr, g)

r := createRequest("POST", "/submit", tt.secureRequest)
r.Header.Set("Origin", "null")
if tt.referer != "" {
r.Header.Set("Referer", tt.referer)
}
tt.token(gr, r, token)

rr := httptest.NewRecorder()
p.ServeHTTP(rr, r)
if rr.Code != tt.want {
t.Fatalf("got status %d, want %d", rr.Code, tt.want)
}
})
}
}

func createRequest(method, path string, useTLS bool) *http.Request {
r := httptest.NewRequest(method, path, nil)
r.Host = "www.gorillatoolkit.org"
Expand Down
Loading