From e7e163eb49f7009212bbb7b9691bcabcb85eed55 Mon Sep 17 00:00:00 2001 From: Charlie Tonneslan Date: Mon, 18 May 2026 15:34:45 -0400 Subject: [PATCH] Treat a literal "null" Origin like a missing Origin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per RFC 6454 §7.3 user agents must send Origin: null from privacy-sensitive contexts (sandboxed iframes, file://, etc.). The opaque value can't be checked against an allowlist, so trying to compare it short-circuits the request with ErrBadOrigin even when the regular Referer-based check would otherwise accept it. Fall through to the existing Referer logic when Origin is "null". For TLS requests that still demands a valid Referer, so the security posture is unchanged. Closes #205 Signed-off-by: Charlie Tonneslan --- csrf.go | 10 ++++++--- csrf_test.go | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 3 deletions(-) diff --git a/csrf.go b/csrf.go index 5dda254..186ec68 100644 --- a/csrf.go +++ b/csrf.go @@ -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) @@ -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()) diff --git a/csrf_test.go b/csrf_test.go index 0281680..528f1d9 100644 --- a/csrf_test.go +++ b/csrf_test.go @@ -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"