@@ -10,6 +10,7 @@ import (
10
10
"context"
11
11
"crypto/rand"
12
12
"embed"
13
+ "encoding/base64"
13
14
"encoding/json"
14
15
"errors"
15
16
"flag"
@@ -29,6 +30,7 @@ import (
29
30
texttemplate "text/template"
30
31
"time"
31
32
33
+ "golang.org/x/net/xsrftoken"
32
34
"tailscale.com/client/tailscale"
33
35
"tailscale.com/hostinfo"
34
36
"tailscale.com/ipn"
@@ -39,8 +41,17 @@ import (
39
41
40
42
const (
41
43
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"
44
55
)
45
56
46
57
var (
200
211
fqdn := strings .TrimSuffix (status .Self .DNSName , "." )
201
212
202
213
httpHandler := serveHandler ()
203
- httpHandler = EnforceSecFetchSiteOrSecGolink (httpHandler )
204
-
205
214
if enableTLS {
206
215
httpsHandler := HSTS (httpHandler )
207
216
httpHandler = redirectHandler (fqdn )
@@ -266,15 +275,19 @@ type homeData struct {
266
275
Short string
267
276
Long string
268
277
Clicks []visitData
278
+ XSRF string
269
279
ReadOnly bool
270
280
}
271
281
272
282
// deleteData is the data used by deleteTmpl.
273
283
type deleteData struct {
274
284
Short string
275
285
Long string
286
+ XSRF string
276
287
}
277
288
289
+ var xsrfKey string
290
+
278
291
func init () {
279
292
homeTmpl = newTemplate ("base.html" , "home.html" )
280
293
detailTmpl = newTemplate ("base.html" , "detail.html" )
@@ -286,6 +299,7 @@ func init() {
286
299
287
300
b := make ([]byte , 24 )
288
301
rand .Read (b )
302
+ xsrfKey = base64 .StdEncoding .EncodeToString (b )
289
303
}
290
304
291
305
var tmplFuncs = template.FuncMap {
@@ -402,34 +416,6 @@ func HSTS(h http.Handler) http.Handler {
402
416
})
403
417
}
404
418
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
-
433
419
// serverHandler returns the main http.Handler for serving all requests.
434
420
func serveHandler () http.Handler {
435
421
mux := http .NewServeMux ()
@@ -490,10 +476,16 @@ func serveHome(w http.ResponseWriter, r *http.Request, short string) {
490
476
}
491
477
}
492
478
479
+ cu , err := currentUser (r )
480
+ if err != nil {
481
+ http .Error (w , err .Error (), http .StatusInternalServerError )
482
+ return
483
+ }
493
484
homeTmpl .Execute (w , homeData {
494
485
Short : short ,
495
486
Long : long ,
496
487
Clicks : clicks ,
488
+ XSRF : xsrftoken .Generate (xsrfKey , cu .login , newShortName ),
497
489
ReadOnly : * readonly ,
498
490
})
499
491
}
@@ -605,6 +597,7 @@ type detailData struct {
605
597
// Editable indicates whether the current user can edit the link.
606
598
Editable bool
607
599
Link * Link
600
+ XSRF string
608
601
}
609
602
610
603
func serveDetail (w http.ResponseWriter , r * http.Request ) {
@@ -648,6 +641,7 @@ func serveDetail(w http.ResponseWriter, r *http.Request) {
648
641
data := detailData {
649
642
Link : link ,
650
643
Editable : canEdit ,
644
+ XSRF : xsrftoken .Generate (xsrfKey , cu .login , link .Short ),
651
645
}
652
646
if canEdit && ! ownerExists {
653
647
data .Link .Owner = cu .login
@@ -835,6 +829,16 @@ func serveDelete(w http.ResponseWriter, r *http.Request) {
835
829
return
836
830
}
837
831
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
+
838
842
if err := db .Delete (short ); err != nil {
839
843
http .Error (w , err .Error (), http .StatusInternalServerError )
840
844
return
@@ -844,6 +848,7 @@ func serveDelete(w http.ResponseWriter, r *http.Request) {
844
848
deleteTmpl .Execute (w , deleteData {
845
849
Short : link .Short ,
846
850
Long : link .Long ,
851
+ XSRF : xsrftoken .Generate (xsrfKey , cu .login , newShortName ),
847
852
})
848
853
}
849
854
@@ -881,15 +886,20 @@ func serveSave(w http.ResponseWriter, r *http.Request) {
881
886
return
882
887
}
883
888
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 )
888
891
return
889
892
}
890
893
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 )
893
903
return
894
904
}
895
905
@@ -1067,3 +1077,14 @@ func resolveLink(link *url.URL) (*url.URL, error) {
1067
1077
}
1068
1078
return dst , err
1069
1079
}
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