@@ -10,6 +10,7 @@ import (
1010	"context" 
1111	"crypto/rand" 
1212	"embed" 
13+ 	"encoding/base64" 
1314	"encoding/json" 
1415	"errors" 
1516	"flag" 
@@ -29,6 +30,7 @@ import (
2930	texttemplate "text/template" 
3031	"time" 
3132
33+ 	"golang.org/x/net/xsrftoken" 
3234	"tailscale.com/client/tailscale" 
3335	"tailscale.com/hostinfo" 
3436	"tailscale.com/ipn" 
@@ -39,8 +41,17 @@ import (
3941
4042const  (
4143	defaultHostname  =  "go" 
42- 	secFetchSite     =  "Sec-Fetch-Site" 
43- 	secGolink        =  "Sec-Golink" 
44+ 
45+ 	// Used as a placeholder short name for generating the XSRF defense token, 
46+ 	// when creating new links. 
47+ 	newShortName  =  ".new" 
48+ 
49+ 	// If the caller sends this header set to a non-empty value, we will allow 
50+ 	// them to make the call even without an XSRF token. JavaScript in browser 
51+ 	// cannot set this header, per the [Fetch Spec]. 
52+ 	// 
53+ 	// [Fetch Spec]: https://fetch.spec.whatwg.org 
54+ 	secHeaderName  =  "Sec-Golink" 
4455)
4556
4657var  (
200211	fqdn  :=  strings .TrimSuffix (status .Self .DNSName , "." )
201212
202213	httpHandler  :=  serveHandler ()
203- 	httpHandler  =  EnforceSecFetchSiteOrSecGolink (httpHandler )
204- 
205214	if  enableTLS  {
206215		httpsHandler  :=  HSTS (httpHandler )
207216		httpHandler  =  redirectHandler (fqdn )
@@ -266,15 +275,19 @@ type homeData struct {
266275	Short     string 
267276	Long      string 
268277	Clicks    []visitData 
278+ 	XSRF      string 
269279	ReadOnly  bool 
270280}
271281
272282// deleteData is the data used by deleteTmpl. 
273283type  deleteData  struct  {
274284	Short  string 
275285	Long   string 
286+ 	XSRF   string 
276287}
277288
289+ var  xsrfKey  string 
290+ 
278291func  init () {
279292	homeTmpl  =  newTemplate ("base.html" , "home.html" )
280293	detailTmpl  =  newTemplate ("base.html" , "detail.html" )
@@ -286,6 +299,7 @@ func init() {
286299
287300	b  :=  make ([]byte , 24 )
288301	rand .Read (b )
302+ 	xsrfKey  =  base64 .StdEncoding .EncodeToString (b )
289303}
290304
291305var  tmplFuncs  =  template.FuncMap {
@@ -402,34 +416,6 @@ func HSTS(h http.Handler) http.Handler {
402416	})
403417}
404418
405- // EnforceSecFetchSiteOrSecGolink is a Cross-Site Request Forgery protection 
406- // middleware that validates the Sec-Fetch-Site header for non-idempotent 
407- // requests. It requires clients to send Sec-Fetch-Site set to "same-origin". 
408- // 
409- // It alternatively allows for clients to send the header "Sec-Golink" set to 
410- // any value to maintain compatibility with clients developed against earlier 
411- // versions of golink that relied on xsrf token based CSRF protection. 
412- func  EnforceSecFetchSiteOrSecGolink (h  http.Handler ) http.Handler  {
413- 	return  http .HandlerFunc (func (w  http.ResponseWriter , r  * http.Request ) {
414- 		switch  r .Method  {
415- 		case  "GET" , "HEAD" , "OPTIONS" : // allow idempotent methods 
416- 			h .ServeHTTP (w , r )
417- 			return 
418- 		}
419- 
420- 		// Check for Sec-Fetch-Site header set to "same-origin" 
421- 		// or Sec-Golink header set to any value for backwards compatibility. 
422- 		sameOrigin  :=  r .Header .Get (secFetchSite ) ==  "same-origin" 
423- 		secGolink  :=  r .Header .Get (secGolink ) !=  "" 
424- 		if  sameOrigin  ||  secGolink  {
425- 			h .ServeHTTP (w , r )
426- 			return 
427- 		}
428- 
429- 		http .Error (w , "invalid non `Sec-Fetch-Site: same-origin` request" , http .StatusBadRequest )
430- 	})
431- }
432- 
433419// serverHandler returns the main http.Handler for serving all requests. 
434420func  serveHandler () http.Handler  {
435421	mux  :=  http .NewServeMux ()
@@ -490,10 +476,16 @@ func serveHome(w http.ResponseWriter, r *http.Request, short string) {
490476		}
491477	}
492478
479+ 	cu , err  :=  currentUser (r )
480+ 	if  err  !=  nil  {
481+ 		http .Error (w , err .Error (), http .StatusInternalServerError )
482+ 		return 
483+ 	}
493484	homeTmpl .Execute (w , homeData {
494485		Short :    short ,
495486		Long :     long ,
496487		Clicks :   clicks ,
488+ 		XSRF :     xsrftoken .Generate (xsrfKey , cu .login , newShortName ),
497489		ReadOnly : * readonly ,
498490	})
499491}
@@ -605,6 +597,7 @@ type detailData struct {
605597	// Editable indicates whether the current user can edit the link. 
606598	Editable  bool 
607599	Link      * Link 
600+ 	XSRF      string 
608601}
609602
610603func  serveDetail (w  http.ResponseWriter , r  * http.Request ) {
@@ -648,6 +641,7 @@ func serveDetail(w http.ResponseWriter, r *http.Request) {
648641	data  :=  detailData {
649642		Link :     link ,
650643		Editable : canEdit ,
644+ 		XSRF :     xsrftoken .Generate (xsrfKey , cu .login , link .Short ),
651645	}
652646	if  canEdit  &&  ! ownerExists  {
653647		data .Link .Owner  =  cu .login 
@@ -835,6 +829,16 @@ func serveDelete(w http.ResponseWriter, r *http.Request) {
835829		return 
836830	}
837831
832+ 	// Deletion by CLI has never worked because it has always required the XSRF 
833+ 	// token. (Refer to commit c7ac33d04c33743606f6224009a5c73aa0b8dec0.) If we 
834+ 	// want to enable deletion via CLI and to honor allowUnknownUsers for 
835+ 	// deletion, we could change the below to a call to isRequestAuthorized. For 
836+ 	// now, always require the XSRF token, thus maintaining the status quo. 
837+ 	if  ! xsrftoken .Valid (r .PostFormValue ("xsrf" ), xsrfKey , cu .login , link .Short ) {
838+ 		http .Error (w , "invalid XSRF token" , http .StatusBadRequest )
839+ 		return 
840+ 	}
841+ 
838842	if  err  :=  db .Delete (short ); err  !=  nil  {
839843		http .Error (w , err .Error (), http .StatusInternalServerError )
840844		return 
@@ -844,6 +848,7 @@ func serveDelete(w http.ResponseWriter, r *http.Request) {
844848	deleteTmpl .Execute (w , deleteData {
845849		Short : link .Short ,
846850		Long :  link .Long ,
851+ 		XSRF :  xsrftoken .Generate (xsrfKey , cu .login , newShortName ),
847852	})
848853}
849854
@@ -881,15 +886,20 @@ func serveSave(w http.ResponseWriter, r *http.Request) {
881886		return 
882887	}
883888
884- 	// Prevent accidental overwrites of existing links. 
885- 	// If the link already exists, make sure this request is an intentional update. 
886- 	if  link  !=  nil  &&  r .FormValue ("update" ) ==  ""  {
887- 		http .Error (w , "link already exists" , http .StatusForbidden )
889+ 	if  ! canEditLink (r .Context (), link , cu ) {
890+ 		http .Error (w , fmt .Sprintf ("cannot update link owned by %q" , link .Owner ), http .StatusForbidden )
888891		return 
889892	}
890893
891- 	if  ! canEditLink (r .Context (), link , cu ) {
892- 		http .Error (w , fmt .Sprintf ("cannot update link owned by %q" , link .Owner ), http .StatusForbidden )
894+ 	// short name to use for XSRF token. 
895+ 	// For new link creation, the special newShortName value is used. 
896+ 	tokenShortName  :=  newShortName 
897+ 	if  link  !=  nil  {
898+ 		tokenShortName  =  link .Short 
899+ 	}
900+ 
901+ 	if  ! isRequestAuthorized (r , cu , tokenShortName ) {
902+ 		http .Error (w , "invalid XSRF token" , http .StatusBadRequest )
893903		return 
894904	}
895905
@@ -1067,3 +1077,14 @@ func resolveLink(link *url.URL) (*url.URL, error) {
10671077	}
10681078	return  dst , err 
10691079}
1080+ 
1081+ func  isRequestAuthorized (r  * http.Request , u  user , short  string ) bool  {
1082+ 	if  * allowUnknownUsers  {
1083+ 		return  true 
1084+ 	}
1085+ 	if  r .Header .Get (secHeaderName ) !=  ""  {
1086+ 		return  true 
1087+ 	}
1088+ 
1089+ 	return  xsrftoken .Valid (r .PostFormValue ("xsrf" ), xsrfKey , u .login , short )
1090+ }
0 commit comments