Преглед изворни кода

client/web: add Sec-Fetch-Site CSRF protection (#16046)

RELNOTE=Fix CSRF errors in the client Web UI

Replace gorilla/csrf with a Sec-Fetch-Site based CSRF protection
middleware that falls back to comparing the Host & Origin headers if no
SFS value is passed by the client.

Add an -origin override to the web CLI that allows callers to specify
the origin at which the web UI will be available if it is hosted behind
a reverse proxy or within another application via CGI.

Updates #14872
Updates #15065

Signed-off-by: Patrick O'Doherty <[email protected]>
Patrick O'Doherty пре 9 месеци
родитељ
комит
a05924a9e5

+ 0 - 10
client/web/src/api.ts

@@ -249,7 +249,6 @@ export function useAPI() {
   return api
 }
 
-let csrfToken: string
 let synoToken: string | undefined // required for synology API requests
 let unraidCsrfToken: string | undefined // required for unraid POST requests (#8062)
 
@@ -298,12 +297,10 @@ export function apiFetch<T>(
     headers: {
       Accept: "application/json",
       "Content-Type": contentType,
-      "X-CSRF-Token": csrfToken,
     },
     body: body,
   })
     .then((r) => {
-      updateCsrfToken(r)
       if (!r.ok) {
         return r.text().then((err) => {
           throw new Error(err)
@@ -322,13 +319,6 @@ export function apiFetch<T>(
     })
 }
 
-function updateCsrfToken(r: Response) {
-  const tok = r.headers.get("X-CSRF-Token")
-  if (tok) {
-    csrfToken = tok
-  }
-}
-
 export function setSynoToken(token?: string) {
   synoToken = token
 }

+ 77 - 60
client/web/web.go

@@ -6,7 +6,6 @@ package web
 
 import (
 	"context"
-	"crypto/rand"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -14,14 +13,14 @@ import (
 	"log"
 	"net/http"
 	"net/netip"
+	"net/url"
 	"os"
 	"path"
-	"path/filepath"
+	"slices"
 	"strings"
 	"sync"
 	"time"
 
-	"github.com/gorilla/csrf"
 	"tailscale.com/client/local"
 	"tailscale.com/client/tailscale/apitype"
 	"tailscale.com/clientupdate"
@@ -60,6 +59,12 @@ type Server struct {
 	cgiMode    bool
 	pathPrefix string
 
+	// originOverride is the origin that the web UI is accessible from.
+	// This value is used in the fallback CSRF checks when Sec-Fetch-Site is not
+	// available. In this case the application will compare Host and Origin
+	// header values to determine if the request is from the same origin.
+	originOverride string
+
 	apiHandler    http.Handler // serves api endpoints; csrf-protected
 	assetsHandler http.Handler // serves frontend assets
 	assetsCleanup func()       // called from Server.Shutdown
@@ -150,6 +155,9 @@ type ServerOpts struct {
 	// as completed.
 	// This field is required for ManageServerMode mode.
 	WaitAuthURL func(ctx context.Context, id string, src tailcfg.NodeID) (*tailcfg.WebClientAuthResponse, error)
+
+	// OriginOverride specifies the origin that the web UI will be accessible from if hosted behind a reverse proxy or CGI.
+	OriginOverride string
 }
 
 // NewServer constructs a new Tailscale web client server.
@@ -169,15 +177,16 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
 		opts.LocalClient = &local.Client{}
 	}
 	s = &Server{
-		mode:        opts.Mode,
-		logf:        opts.Logf,
-		devMode:     envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
-		lc:          opts.LocalClient,
-		cgiMode:     opts.CGIMode,
-		pathPrefix:  opts.PathPrefix,
-		timeNow:     opts.TimeNow,
-		newAuthURL:  opts.NewAuthURL,
-		waitAuthURL: opts.WaitAuthURL,
+		mode:           opts.Mode,
+		logf:           opts.Logf,
+		devMode:        envknob.Bool("TS_DEBUG_WEB_CLIENT_DEV"),
+		lc:             opts.LocalClient,
+		cgiMode:        opts.CGIMode,
+		pathPrefix:     opts.PathPrefix,
+		timeNow:        opts.TimeNow,
+		newAuthURL:     opts.NewAuthURL,
+		waitAuthURL:    opts.WaitAuthURL,
+		originOverride: opts.OriginOverride,
 	}
 	if opts.PathPrefix != "" {
 		// Enforce that path prefix always has a single leading '/'
@@ -205,7 +214,7 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
 
 	var metric string
 	s.apiHandler, metric = s.modeAPIHandler(s.mode)
-	s.apiHandler = s.withCSRF(s.apiHandler)
+	s.apiHandler = s.csrfProtect(s.apiHandler)
 
 	// Don't block startup on reporting metric.
 	// Report in separate go routine with 5 second timeout.
@@ -218,23 +227,64 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
 	return s, nil
 }
 
-func (s *Server) withCSRF(h http.Handler) http.Handler {
-	csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
+func (s *Server) csrfProtect(h http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		// CSRF is not required for GET, HEAD, or OPTIONS requests.
+		if slices.Contains([]string{"GET", "HEAD", "OPTIONS"}, r.Method) {
+			h.ServeHTTP(w, r)
+			return
+		}
 
-	// ref https://github.com/tailscale/tailscale/pull/14822
-	// signal to the CSRF middleware that the request is being served over
-	// plaintext HTTP to skip TLS-only header checks.
-	withSetPlaintext := func(h http.Handler) http.Handler {
-		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			r = csrf.PlaintextHTTPRequest(r)
+		// first attempt to use Sec-Fetch-Site header (sent by all modern
+		// browsers to "potentially trustworthy" origins i.e. localhost or those
+		// served over HTTPS)
+		secFetchSite := r.Header.Get("Sec-Fetch-Site")
+		if secFetchSite == "same-origin" {
 			h.ServeHTTP(w, r)
-		})
-	}
+			return
+		} else if secFetchSite != "" {
+			http.Error(w, fmt.Sprintf("CSRF request denied with Sec-Fetch-Site %q", secFetchSite), http.StatusForbidden)
+			return
+		}
+
+		// if Sec-Fetch-Site is not available we presume we are operating over HTTP.
+		// We fall back to comparing the Origin & Host headers.
+
+		// use the Host header to determine the expected origin
+		// (use the override if set to allow for reverse proxying)
+		host := r.Host
+		if host == "" {
+			http.Error(w, "CSRF request denied with no Host header", http.StatusForbidden)
+			return
+		}
+		if s.originOverride != "" {
+			host = s.originOverride
+		}
+
+		originHeader := r.Header.Get("Origin")
+		if originHeader == "" {
+			http.Error(w, "CSRF request denied with no Origin header", http.StatusForbidden)
+			return
+		}
+		parsedOrigin, err := url.Parse(originHeader)
+		if err != nil {
+			http.Error(w, fmt.Sprintf("CSRF request denied with invalid Origin %q", r.Header.Get("Origin")), http.StatusForbidden)
+			return
+		}
+		origin := parsedOrigin.Host
+		if origin == "" {
+			http.Error(w, "CSRF request denied with no host in the Origin header", http.StatusForbidden)
+			return
+		}
+
+		if origin != host {
+			http.Error(w, fmt.Sprintf("CSRF request denied with mismatched Origin %q and Host %q", origin, host), http.StatusForbidden)
+			return
+		}
+
+		h.ServeHTTP(w, r)
 
-	// NB: the order of the withSetPlaintext and csrfProtect calls is important
-	// to ensure that we signal to the CSRF middleware that the request is being
-	// served over plaintext HTTP and not over TLS as it presumes by default.
-	return withSetPlaintext(csrfProtect(h))
+	})
 }
 
 func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
@@ -452,7 +502,6 @@ func (s *Server) authorizeRequest(w http.ResponseWriter, r *http.Request) (ok bo
 // It should only be called by Server.ServeHTTP, via Server.apiHandler,
 // which protects the handler using gorilla csrf.
 func (s *Server) serveLoginAPI(w http.ResponseWriter, r *http.Request) {
-	w.Header().Set("X-CSRF-Token", csrf.Token(r))
 	switch {
 	case r.URL.Path == "/api/data" && r.Method == httpm.GET:
 		s.serveGetNodeData(w, r)
@@ -575,7 +624,6 @@ func (s *Server) serveAPI(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	w.Header().Set("X-CSRF-Token", csrf.Token(r))
 	path := strings.TrimPrefix(r.URL.Path, "/api")
 	switch {
 	case path == "/data" && r.Method == httpm.GET:
@@ -1276,37 +1324,6 @@ func (s *Server) proxyRequestToLocalAPI(w http.ResponseWriter, r *http.Request)
 	}
 }
 
-// csrfKey returns a key that can be used for CSRF protection.
-// If an error occurs during key creation, the error is logged and the active process terminated.
-// If the server is running in CGI mode, the key is cached to disk and reused between requests.
-// If an error occurs during key storage, the error is logged and the active process terminated.
-func (s *Server) csrfKey() []byte {
-	csrfFile := filepath.Join(os.TempDir(), "tailscale-web-csrf.key")
-
-	// if running in CGI mode, try to read from disk, but ignore errors
-	if s.cgiMode {
-		key, _ := os.ReadFile(csrfFile)
-		if len(key) == 32 {
-			return key
-		}
-	}
-
-	// create a new key
-	key := make([]byte, 32)
-	if _, err := rand.Read(key); err != nil {
-		log.Fatalf("error generating CSRF key: %v", err)
-	}
-
-	// if running in CGI mode, try to write the newly created key to disk, and exit if it fails.
-	if s.cgiMode {
-		if err := os.WriteFile(csrfFile, key, 0600); err != nil {
-			log.Fatalf("unable to store CSRF key: %v", err)
-		}
-	}
-
-	return key
-}
-
 // enforcePrefix returns a HandlerFunc that enforces a given path prefix is used in requests,
 // then strips it before invoking h.
 // Unlike http.StripPrefix, it does not return a 404 if the prefix is not present.

+ 89 - 74
client/web/web_test.go

@@ -11,7 +11,6 @@ import (
 	"fmt"
 	"io"
 	"net/http"
-	"net/http/cookiejar"
 	"net/http/httptest"
 	"net/netip"
 	"net/url"
@@ -21,14 +20,12 @@ import (
 	"time"
 
 	"github.com/google/go-cmp/cmp"
-	"github.com/gorilla/csrf"
 	"tailscale.com/client/local"
 	"tailscale.com/client/tailscale/apitype"
 	"tailscale.com/ipn"
 	"tailscale.com/ipn/ipnstate"
 	"tailscale.com/net/memnet"
 	"tailscale.com/tailcfg"
-	"tailscale.com/tstest/nettest"
 	"tailscale.com/types/views"
 	"tailscale.com/util/httpm"
 )
@@ -1492,81 +1489,99 @@ func mockWaitAuthURL(_ context.Context, id string, src tailcfg.NodeID) (*tailcfg
 }
 
 func TestCSRFProtect(t *testing.T) {
-	s := &Server{}
-
-	mux := http.NewServeMux()
-	mux.HandleFunc("GET /test/csrf-token", func(w http.ResponseWriter, r *http.Request) {
-		token := csrf.Token(r)
-		_, err := io.WriteString(w, token)
-		if err != nil {
-			t.Fatal(err)
-		}
-	})
-	mux.HandleFunc("POST /test/csrf-protected", func(w http.ResponseWriter, r *http.Request) {
-		_, err := io.WriteString(w, "ok")
-		if err != nil {
-			t.Fatal(err)
-		}
-	})
-	h := s.withCSRF(mux)
-	ser := nettest.NewHTTPServer(nettest.GetNetwork(t), h)
-	defer ser.Close()
-
-	jar, err := cookiejar.New(nil)
-	if err != nil {
-		t.Fatalf("unable to construct cookie jar: %v", err)
+	tests := []struct {
+		name           string
+		method         string
+		secFetchSite   string
+		host           string
+		origin         string
+		originOverride string
+		wantError      bool
+	}{
+		{
+			name:   "GET requests with no header are allowed",
+			method: "GET",
+		},
+		{
+			name:         "POST requests with same-origin are allowed",
+			method:       "POST",
+			secFetchSite: "same-origin",
+		},
+		{
+			name:         "POST requests with cross-site are not allowed",
+			method:       "POST",
+			secFetchSite: "cross-site",
+			wantError:    true,
+		},
+		{
+			name:         "POST requests with unknown sec-fetch-site values are not allowed",
+			method:       "POST",
+			secFetchSite: "new-unknown-value",
+			wantError:    true,
+		},
+		{
+			name:         "POST requests with none are not allowed",
+			method:       "POST",
+			secFetchSite: "none",
+			wantError:    true,
+		},
+		{
+			name:   "POST requests with no sec-fetch-site header but matching host and origin are allowed",
+			method: "POST",
+			host:   "example.com",
+			origin: "https://example.com",
+		},
+		{
+			name:      "POST requests with no sec-fetch-site and non-matching host and origin are not allowed",
+			method:    "POST",
+			host:      "example.com",
+			origin:    "https://example.net",
+			wantError: true,
+		},
+		{
+			name:           "POST requests with no sec-fetch-site and and origin that matches the override are allowed",
+			method:         "POST",
+			originOverride: "example.net",
+			host:           "internal.example.foo", // Host can be changed by reverse proxies
+			origin:         "http://example.net",
+		},
 	}
 
-	client := ser.Client()
-	client.Jar = jar
-
-	// make GET request to populate cookie jar
-	resp, err := client.Get(ser.URL + "/test/csrf-token")
-	if err != nil {
-		t.Fatalf("unable to make request: %v", err)
-	}
-	defer resp.Body.Close()
-	if resp.StatusCode != http.StatusOK {
-		t.Fatalf("unexpected status: %v", resp.Status)
-	}
-	tokenBytes, err := io.ReadAll(resp.Body)
-	if err != nil {
-		t.Fatalf("unable to read body: %v", err)
-	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				fmt.Fprintf(w, "OK")
+			})
 
-	csrfToken := strings.TrimSpace(string(tokenBytes))
-	if csrfToken == "" {
-		t.Fatal("empty csrf token")
-	}
+			s := &Server{
+				originOverride: tt.originOverride,
+			}
+			withCSRF := s.csrfProtect(handler)
 
-	// make a POST request without the CSRF header; ensure it fails
-	resp, err = client.Post(ser.URL+"/test/csrf-protected", "text/plain", nil)
-	if err != nil {
-		t.Fatalf("unable to make request: %v", err)
-	}
-	if resp.StatusCode != http.StatusForbidden {
-		t.Fatalf("unexpected status: %v", resp.Status)
-	}
+			r := httptest.NewRequest(tt.method, "http://example.com/", nil)
+			if tt.secFetchSite != "" {
+				r.Header.Set("Sec-Fetch-Site", tt.secFetchSite)
+			}
+			if tt.host != "" {
+				r.Host = tt.host
+			}
+			if tt.origin != "" {
+				r.Header.Set("Origin", tt.origin)
+			}
 
-	// make a POST request with the CSRF header; ensure it succeeds
-	req, err := http.NewRequest("POST", ser.URL+"/test/csrf-protected", nil)
-	if err != nil {
-		t.Fatalf("error building request: %v", err)
-	}
-	req.Header.Set("X-CSRF-Token", csrfToken)
-	resp, err = client.Do(req)
-	if err != nil {
-		t.Fatalf("unable to make request: %v", err)
-	}
-	if resp.StatusCode != http.StatusOK {
-		t.Fatalf("unexpected status: %v", resp.Status)
-	}
-	defer resp.Body.Close()
-	out, err := io.ReadAll(resp.Body)
-	if err != nil {
-		t.Fatalf("unable to read body: %v", err)
-	}
-	if string(out) != "ok" {
-		t.Fatalf("unexpected body: %q", out)
+			w := httptest.NewRecorder()
+			withCSRF.ServeHTTP(w, r)
+			res := w.Result()
+			defer res.Body.Close()
+			if tt.wantError {
+				if res.StatusCode != http.StatusForbidden {
+					t.Errorf("expected status forbidden, got %v", res.StatusCode)
+				}
+				return
+			}
+			if res.StatusCode != http.StatusOK {
+				t.Errorf("expected status ok, got %v", res.StatusCode)
+			}
+		})
 	}
 }

+ 3 - 6
cmd/k8s-operator/depaware.txt

@@ -144,8 +144,6 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
    L    github.com/google/nftables/internal/parseexprfunc            from github.com/google/nftables+
    L    github.com/google/nftables/xt                                from github.com/google/nftables/expr+
         github.com/google/uuid                                       from github.com/prometheus-community/pro-bing+
-        github.com/gorilla/csrf                                      from tailscale.com/client/web
-        github.com/gorilla/securecookie                              from github.com/gorilla/csrf
         github.com/hdevalence/ed25519consensus                       from tailscale.com/clientupdate/distsign+
    L 💣 github.com/illarion/gonotify/v3                              from tailscale.com/net/dns
    L    github.com/illarion/gonotify/v3/syscallf                     from github.com/illarion/gonotify/v3
@@ -1112,13 +1110,12 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
    W    debug/dwarf                                                  from debug/pe
    W    debug/pe                                                     from github.com/dblohm7/wingoes/pe
         embed                                                        from github.com/tailscale/web-client-prebuilt+
-        encoding                                                     from encoding/gob+
+        encoding                                                     from encoding/json+
         encoding/asn1                                                from crypto/x509+
         encoding/base32                                              from github.com/fxamacker/cbor/v2+
         encoding/base64                                              from encoding/json+
         encoding/binary                                              from compress/gzip+
         encoding/csv                                                 from github.com/spf13/pflag
-        encoding/gob                                                 from github.com/gorilla/securecookie
         encoding/hex                                                 from crypto/x509+
         encoding/json                                                from expvar+
         encoding/pem                                                 from crypto/tls+
@@ -1140,7 +1137,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
         hash/fnv                                                     from google.golang.org/protobuf/internal/detrand
         hash/maphash                                                 from go4.org/mem
         html                                                         from html/template+
-        html/template                                                from github.com/gorilla/csrf+
+        html/template                                                from tailscale.com/util/eventbus
         internal/abi                                                 from crypto/x509/internal/macos+
         internal/asan                                                from internal/runtime/maps+
         internal/bisect                                              from internal/godebug
@@ -1172,7 +1169,7 @@ tailscale.com/cmd/k8s-operator dependencies: (generated by github.com/tailscale/
         internal/runtime/math                                        from internal/runtime/maps+
         internal/runtime/sys                                         from crypto/subtle+
    L    internal/runtime/syscall                                     from runtime+
-        internal/saferio                                             from debug/pe+
+   W    internal/saferio                                             from debug/pe
         internal/singleflight                                        from net
         internal/stringslite                                         from embed+
         internal/sync                                                from sync+

+ 5 - 0
cmd/tailscale/cli/web.go

@@ -43,6 +43,7 @@ Tailscale, as opposed to a CLI or a native app.
 		webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
 		webf.StringVar(&webArgs.prefix, "prefix", "", "URL prefix added to requests (for cgi or reverse proxies)")
 		webf.BoolVar(&webArgs.readonly, "readonly", false, "run web UI in read-only mode")
+		webf.StringVar(&webArgs.origin, "origin", "", "origin at which the web UI is served (if behind a reverse proxy or used with cgi)")
 		return webf
 	})(),
 	Exec: runWeb,
@@ -53,6 +54,7 @@ var webArgs struct {
 	cgi      bool
 	prefix   string
 	readonly bool
+	origin   string
 }
 
 func tlsConfigFromEnvironment() *tls.Config {
@@ -115,6 +117,9 @@ func runWeb(ctx context.Context, args []string) error {
 	if webArgs.readonly {
 		opts.Mode = web.ReadOnlyServerMode
 	}
+	if webArgs.origin != "" {
+		opts.OriginOverride = webArgs.origin
+	}
 	webServer, err := web.NewServer(opts)
 	if err != nil {
 		log.Printf("tailscale.web: %v", err)

+ 3 - 6
cmd/tailscale/depaware.txt

@@ -27,8 +27,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
    L    github.com/google/nftables/internal/parseexprfunc            from github.com/google/nftables+
    L    github.com/google/nftables/xt                                from github.com/google/nftables/expr+
   DW    github.com/google/uuid                                       from tailscale.com/clientupdate+
-        github.com/gorilla/csrf                                      from tailscale.com/client/web
-        github.com/gorilla/securecookie                              from github.com/gorilla/csrf
         github.com/hdevalence/ed25519consensus                       from tailscale.com/clientupdate/distsign+
    L 💣 github.com/jsimonetti/rtnetlink                              from tailscale.com/net/netmon
    L    github.com/jsimonetti/rtnetlink/internal/unix                from github.com/jsimonetti/rtnetlink
@@ -319,12 +317,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
    W    debug/dwarf                                                  from debug/pe
    W    debug/pe                                                     from github.com/dblohm7/wingoes/pe
         embed                                                        from github.com/peterbourgon/ff/v3+
-        encoding                                                     from encoding/gob+
+        encoding                                                     from encoding/json+
         encoding/asn1                                                from crypto/x509+
         encoding/base32                                              from github.com/fxamacker/cbor/v2+
         encoding/base64                                              from encoding/json+
         encoding/binary                                              from compress/gzip+
-        encoding/gob                                                 from github.com/gorilla/securecookie
         encoding/hex                                                 from crypto/x509+
         encoding/json                                                from expvar+
         encoding/pem                                                 from crypto/tls+
@@ -338,7 +335,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         hash/crc32                                                   from compress/gzip+
         hash/maphash                                                 from go4.org/mem
         html                                                         from html/template+
-        html/template                                                from github.com/gorilla/csrf+
+        html/template                                                from tailscale.com/util/eventbus
         image                                                        from github.com/skip2/go-qrcode+
         image/color                                                  from github.com/skip2/go-qrcode+
         image/png                                                    from github.com/skip2/go-qrcode
@@ -372,7 +369,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
         internal/runtime/math                                        from internal/runtime/maps+
         internal/runtime/sys                                         from crypto/subtle+
    L    internal/runtime/syscall                                     from runtime+
-        internal/saferio                                             from debug/pe+
+   W    internal/saferio                                             from debug/pe
         internal/singleflight                                        from net
         internal/stringslite                                         from embed+
         internal/sync                                                from sync+

+ 3 - 6
cmd/tailscaled/depaware.txt

@@ -123,8 +123,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
    L    github.com/google/nftables/internal/parseexprfunc            from github.com/google/nftables+
    L    github.com/google/nftables/xt                                from github.com/google/nftables/expr+
   DW    github.com/google/uuid                                       from tailscale.com/clientupdate+
-        github.com/gorilla/csrf                                      from tailscale.com/client/web
-        github.com/gorilla/securecookie                              from github.com/gorilla/csrf
         github.com/hdevalence/ed25519consensus                       from tailscale.com/clientupdate/distsign+
    L 💣 github.com/illarion/gonotify/v3                              from tailscale.com/net/dns
    L    github.com/illarion/gonotify/v3/syscallf                     from github.com/illarion/gonotify/v3
@@ -590,12 +588,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
    W    debug/dwarf                                                  from debug/pe
    W    debug/pe                                                     from github.com/dblohm7/wingoes/pe
         embed                                                        from github.com/tailscale/web-client-prebuilt+
-        encoding                                                     from encoding/gob+
+        encoding                                                     from encoding/json+
         encoding/asn1                                                from crypto/x509+
         encoding/base32                                              from github.com/fxamacker/cbor/v2+
         encoding/base64                                              from encoding/json+
         encoding/binary                                              from compress/gzip+
-        encoding/gob                                                 from github.com/gorilla/securecookie
         encoding/hex                                                 from crypto/x509+
         encoding/json                                                from expvar+
         encoding/pem                                                 from crypto/tls+
@@ -609,7 +606,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         hash/crc32                                                   from compress/gzip+
         hash/maphash                                                 from go4.org/mem
         html                                                         from html/template+
-        html/template                                                from github.com/gorilla/csrf+
+        html/template                                                from tailscale.com/util/eventbus
         internal/abi                                                 from crypto/x509/internal/macos+
         internal/asan                                                from internal/runtime/maps+
         internal/bisect                                              from internal/godebug
@@ -640,7 +637,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
         internal/runtime/math                                        from internal/runtime/maps+
         internal/runtime/sys                                         from crypto/subtle+
    L    internal/runtime/syscall                                     from runtime+
-        internal/saferio                                             from debug/pe+
+   W    internal/saferio                                             from debug/pe
         internal/singleflight                                        from net
         internal/stringslite                                         from embed+
         internal/sync                                                from sync+

+ 3 - 6
tsnet/depaware.txt

@@ -113,8 +113,6 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
    L    github.com/google/nftables/internal/parseexprfunc            from github.com/google/nftables+
    L    github.com/google/nftables/xt                                from github.com/google/nftables/expr+
  DWI    github.com/google/uuid                                       from github.com/prometheus-community/pro-bing+
- LDW    github.com/gorilla/csrf                                      from tailscale.com/client/web
- LDW    github.com/gorilla/securecookie                              from github.com/gorilla/csrf
         github.com/hdevalence/ed25519consensus                       from tailscale.com/clientupdate/distsign+
    L 💣 github.com/illarion/gonotify/v3                              from tailscale.com/net/dns
    L    github.com/illarion/gonotify/v3/syscallf                     from github.com/illarion/gonotify/v3
@@ -534,12 +532,11 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
    W    debug/dwarf                                                  from debug/pe
    W    debug/pe                                                     from github.com/dblohm7/wingoes/pe
         embed                                                        from github.com/tailscale/web-client-prebuilt+
-        encoding                                                     from encoding/gob+
+        encoding                                                     from encoding/json+
         encoding/asn1                                                from crypto/x509+
         encoding/base32                                              from github.com/fxamacker/cbor/v2+
         encoding/base64                                              from encoding/json+
         encoding/binary                                              from compress/gzip+
- LDW    encoding/gob                                                 from github.com/gorilla/securecookie
         encoding/hex                                                 from crypto/x509+
         encoding/json                                                from expvar+
         encoding/pem                                                 from crypto/tls+
@@ -553,7 +550,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
         hash/crc32                                                   from compress/gzip+
         hash/maphash                                                 from go4.org/mem
         html                                                         from html/template+
- LDW    html/template                                                from github.com/gorilla/csrf+
+ LDW    html/template                                                from tailscale.com/util/eventbus
         internal/abi                                                 from crypto/x509/internal/macos+
         internal/asan                                                from internal/runtime/maps+
         internal/bisect                                              from internal/godebug
@@ -584,7 +581,7 @@ tailscale.com/tsnet dependencies: (generated by github.com/tailscale/depaware)
         internal/runtime/math                                        from internal/runtime/maps+
         internal/runtime/sys                                         from crypto/subtle+
   LA    internal/runtime/syscall                                     from runtime+
- LDW    internal/saferio                                             from debug/pe+
+   W    internal/saferio                                             from debug/pe
         internal/singleflight                                        from net
         internal/stringslite                                         from embed+
         internal/sync                                                from sync+