Tell the browser exactly who may call you and how to treat your responses โ over an encrypted connection.
9a
Before a cross-origin PATCH with a token, the browser sends an OPTIONS and waits for permission.
func CORS(allowed []string, next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") w.Header().Add("Vary", "Origin") if origin != "" && slices.Contains(allowed, origin) { h := w.Header() h.Set("Access-Control-Allow-Origin", origin) h.Set("Access-Control-Allow-Credentials", "true") if r.Method == http.MethodOptions { h.Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE") h.Set("Access-Control-Allow-Headers", "Authorization, Content-Type") h.Set("Access-Control-Max-Age", "600") w.WriteHeader(http.StatusNoContent) return } } if r.Method == http.MethodOptions { w.WriteHeader(http.StatusNoContent) // no CORS headers return } next.ServeHTTP(w, r) } return http.HandlerFunc(fn) } // allowlist comes from config (session 10) h := CORS([]string{"https://app.example.com"}, mux)
OPTIONS from https://app.example.com โ 204 Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, PATCH, DELETE Access-Control-Allow-Headers: Authorization, Content-Type Vary: Origin OPTIONS from https://evil.example โ 204 Vary: Origin # no Allow-Origin header โ the browser refuses to send the PATCH
Allow-Origin: * with credentials is rejected by browsers โ and reflecting any Origin back is worse. Use an exact allowlist.Vary: Origin stops a cache serving one origin's CORS answer to another.curl ignores it. Auth still runs on every request.9b
Each one switches off a browser behaviour an attacker would like to use.
func SecureHeaders(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { h := w.Header() h.Set("X-Content-Type-Options", "nosniff") h.Set("X-Frame-Options", "DENY") h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'") h.Set("Referrer-Policy", "no-referrer") h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains") next.ServeHTTP(w, r) } return http.HandlerFunc(fn) }
default-src 'none' โ because it serves no HTML.next.ServeHTTP โ once the body starts, headers are sent.9c
Either Go serves HTTPS itself, or a load balancer does and forwards plain HTTP on a private network.
srv := &http.Server{ Addr: ":443", Handler: h, ReadHeaderTimeout: 5 * time.Second, // slow-loris guard ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, TLSConfig: &tls.Config{MinVersion: tls.VersionTLS12}, } go http.ListenAndServe(":80", http.HandlerFunc(redirect)) log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem")) func redirect(w http.ResponseWriter, r *http.Request) { u := "https://" + r.Host + r.URL.RequestURI() http.Redirect(w, r, u, http.StatusMovedPermanently) }
proto: HTTP/1.1 tls: TLS 1.3 cipher: TLS_AES_128_GCM_SHA256
plain http โ 400 Client sent an HTTP request to an HTTPS server.
untrusted cert: Get "https://127.0.0.1:39139/users/me": tls: failed to verify certificate: x509: certificate signed by unknown authority
301 https://api.example.com/users/me?x=1 # the :80 redirect
http.Server has no timeouts by default โ always set them.max-age.InsecureSkipVerify: true in a client โ it turns TLS off without saying so.SecureHeaders(CORS(allowed, mux)), allowlist from an env var.Allow-Origin; any other origin doesn't.httptest.NewTLSServer and assert res.TLS.Version.Recap
Vary: Origin, credentials only for listed origins.Secure; HttpOnly; SameSite.