package main import ( "net/http/httptest" "strings" "testing" "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/cors" ) // Cross-origin access, checked the way a browser checks it. // // These exist because this went wrong in production and nothing caught it. // Adding the session token to the console made every request non-simple, so // browsers began asking permission first — and the answer did not name the // `Authorization` header, so every call was blocked. // // The reason it reached production is worth keeping in mind while reading // these: the preflight returns 204 and looks perfectly healthy from a terminal, // the server logs nothing unusual, and `curl` cannot reproduce it because curl // does not enforce CORS. The only thing that noticed was a browser. // preflight asks the question a browser asks before a cross-origin request. func preflight(t *testing.T, requestHeaders string) map[string]string { t.Helper() app := fiber.New() app.Use(cors.New(corsSettings())) app.Get("/probe", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }) req := httptest.NewRequest("OPTIONS", "/probe", nil) req.Header.Set("Origin", "https://app.nearledaily.com") req.Header.Set("Access-Control-Request-Method", "GET") if requestHeaders != "" { req.Header.Set("Access-Control-Request-Headers", requestHeaders) } resp, err := app.Test(req, -1) if err != nil { t.Fatalf("preflight: %v", err) } out := map[string]string{} for _, name := range []string{ "Access-Control-Allow-Origin", "Access-Control-Allow-Headers", "Access-Control-Allow-Methods", "Access-Control-Allow-Credentials", } { out[name] = resp.Header.Get(name) } return out } func TestTheBrowserIsAllowedToSendTheSessionToken(t *testing.T) { // The bug itself. Without `Authorization` in this list the console cannot // make a single authenticated call, and the error surfaces only in a // browser console as a CORS failure. headers := preflight(t, "authorization")["Access-Control-Allow-Headers"] if !strings.Contains(strings.ToLower(headers), "authorization") { t.Fatalf("the console may not send its session token: %q", headers) } } func TestTheHeadersTheConsoleAlreadySentStillWork(t *testing.T) { // Adding one header must not quietly drop the others. headers := strings.ToLower(preflight(t, "content-type")["Access-Control-Allow-Headers"]) for _, needed := range []string{"content-type", "accept", "origin"} { if !strings.Contains(headers, needed) { t.Fatalf("%q is no longer allowed: %q", needed, headers) } } } func TestAWildcardOriginIsNotPairedWithCredentials(t *testing.T) { // Not a valid combination: a browser rejects a credentialed response // carrying a wildcard origin. It was here already and was harmless only // because nothing used credentials — it would have become a second bug // that looked exactly like the first, the day anything did. got := preflight(t, "authorization") if got["Access-Control-Allow-Origin"] == "*" && strings.EqualFold(got["Access-Control-Allow-Credentials"], "true") { t.Fatal("wildcard origin with credentials allowed — browsers reject this pair") } } func TestEveryMethodTheConsoleUsesIsAllowed(t *testing.T) { // The console writes with POST, PUT and DELETE. A missing one fails only // on the screens that use it, which is the kind of gap that ships. methods := strings.ToUpper(preflight(t, "authorization")["Access-Control-Allow-Methods"]) for _, method := range []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"} { if !strings.Contains(methods, method) { t.Fatalf("%s is not allowed cross-origin: %q", method, methods) } } } func TestAResponseHeaderIsNotListedAsAnAllowedRequestHeader(t *testing.T) { // `Access-Control-Allow-Origin` was in the allowed REQUEST headers, which is // a category error: it is something the server sends back, never something a // browser asks to send. Harmless, but it reads as though somebody added // names until the error went away. headers := strings.ToLower(preflight(t, "authorization")["Access-Control-Allow-Headers"]) if strings.Contains(headers, "access-control-allow-origin") { t.Fatalf("a response header is listed as an allowed request header: %q", headers) } }