From 1ea19c5a6c7887af899a0c08c6317ac8c9a62d74 Mon Sep 17 00:00:00 2001 From: WeebDataHoarder Date: Sat, 3 May 2025 04:00:17 +0200 Subject: [PATCH] state context: Added proxy-safe-link-tags to proxy tags, use specific LinkTags ranger on templates instead of raw elements --- embed/templates/challenge-anubis.gohtml | 5 +- embed/templates/challenge-forgejo.gohtml | 5 +- examples/forgejo.yml | 1 + lib/challenge/data.go | 5 +- lib/challenge/refresh/refresh.go | 2 +- lib/challenge/resource-load/resource-load.go | 10 +- lib/http.go | 168 +++++++++++++------ lib/template.go | 54 +++--- utils/tagfetcher.go | 5 +- 9 files changed, 163 insertions(+), 92 deletions(-) diff --git a/embed/templates/challenge-anubis.gohtml b/embed/templates/challenge-anubis.gohtml index ca03d94..d2791da 100644 --- a/embed/templates/challenge-anubis.gohtml +++ b/embed/templates/challenge-anubis.gohtml @@ -5,9 +5,12 @@ - {{ range .Meta }} + {{ range .MetaTags }} {{ end }} + {{ range .LinkTags }} + + {{ end }} {{ range .HeaderTags }} {{ . }} {{ end }} diff --git a/embed/templates/challenge-forgejo.gohtml b/embed/templates/challenge-forgejo.gohtml index ffc6739..824157d 100644 --- a/embed/templates/challenge-forgejo.gohtml +++ b/embed/templates/challenge-forgejo.gohtml @@ -6,9 +6,12 @@ {{ .Title }} - {{ range .Meta }} + {{ range .MetaTags }} {{ end }} + {{ range .LinkTags }} + + {{ end }} {{ range .HeaderTags }} {{ . }} {{ end }} diff --git a/examples/forgejo.yml b/examples/forgejo.yml index d2de0c1..0e66798 100644 --- a/examples/forgejo.yml +++ b/examples/forgejo.yml @@ -293,6 +293,7 @@ rules: context-set: # Map OpenGraph or similar tags back to the reply, even if denied/challenged proxy-meta-tags: "true" + # proxy-safe-link-tags: "true" # Set additional response headers #response-headers: diff --git a/lib/challenge/data.go b/lib/challenge/data.go index 0b49fe3..cc52627 100644 --- a/lib/challenge/data.go +++ b/lib/challenge/data.go @@ -152,8 +152,9 @@ func (d *RequestData) NetworkPrefix() netip.Addr { } const ( - RequestOptBackendHost = "backend-host" - RequestOptCacheMetaTags = "proxy-meta-tags" + RequestOptBackendHost = "backend-host" + RequestOptProxyMetaTags = "proxy-meta-tags" + RequestOptProxySafeLinkTags = "proxy-safe-link-tags" ) func (d *RequestData) SetOpt(n, v string) { diff --git a/lib/challenge/refresh/refresh.go b/lib/challenge/refresh/refresh.go index 50be69b..dfb1426 100644 --- a/lib/challenge/refresh/refresh.go +++ b/lib/challenge/refresh/refresh.go @@ -47,7 +47,7 @@ func FillRegistration(state challenge.StateInterface, reg *challenge.Registratio if params.Mode == "meta" { state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{ - "Meta": []map[string]string{ + "MetaTags": []map[string]string{ { "http-equiv": "refresh", "content": "0; url=" + uri.String(), diff --git a/lib/challenge/resource-load/resource-load.go b/lib/challenge/resource-load/resource-load.go index 0fb632a..4c3e232 100644 --- a/lib/challenge/resource-load/resource-load.go +++ b/lib/challenge/resource-load/resource-load.go @@ -1,10 +1,8 @@ package resource_load import ( - "fmt" "git.gammaspectra.live/git/go-away/lib/challenge" "github.com/goccy/go-yaml/ast" - "html/template" "net/http" "time" ) @@ -30,8 +28,12 @@ func FillRegistrationHeader(state challenge.StateInterface, reg *challenge.Regis w.Header().Set("Refresh", "2; url="+r.URL.String()) state.ChallengePage(w, r, state.Settings().ChallengeResponseCode, reg, map[string]any{ - "HeaderTags": []template.HTML{ - template.HTML(fmt.Sprintf("", uri.String())), + "LinkTags": []map[string]string{ + { + "href": uri.String(), + "rel": "stylesheet", + "crossorigin": "use-credentials", + }, }, }) return challenge.VerifyResultNone diff --git a/lib/http.go b/lib/http.go index 8ae60f3..634e055 100644 --- a/lib/http.go +++ b/lib/http.go @@ -38,7 +38,7 @@ func GetLoggerForRequest(r *http.Request) *slog.Logger { return slog.With(args...) } -func (state *State) fetchMetaTags(host string, backend http.Handler, r *http.Request) []html.Node { +func (state *State) fetchTags(host string, backend http.Handler, r *http.Request, meta, link bool) []html.Node { uri := *r.URL q := uri.Query() for k := range q { @@ -54,76 +54,142 @@ func (state *State) fetchMetaTags(host string, backend http.Handler, r *http.Req return v } - result := utils.FetchTags(backend, &uri, "meta") + result := utils.FetchTags(backend, &uri, func() (r []string) { + if meta { + r = append(r, "meta") + } else if link { + r = append(r, "link") + } + return r + }()...) if result == nil { return nil } entries := make([]html.Node, 0, len(result)) - - safeAttributes := []string{"name", "property", "content"} for _, n := range result { if n.Namespace != "" { continue } - var name string - for _, attr := range n.Attr { - if attr.Namespace != "" { - continue - } - if attr.Key == "name" { - name = attr.Val - break - } - if attr.Key == "property" && name == "" { - name = attr.Val - } - } + switch n.Data { + case "link": + safeAttributes := []string{"rel", "href", "hreflang", "media", "title", "type"} - // prevent unwanted keys like CSRF and other internal entries to pass through as much as possible - - var keep bool - if strings.HasPrefix("og:", name) || strings.HasPrefix("fb:", name) || strings.HasPrefix("twitter:", name) || strings.HasPrefix("profile:", name) { - // social / OpenGraph tags - keep = true - } else if name == "vcs" || strings.HasPrefix("vcs:", name) { - // source tags - keep = true - } else if name == "forge" || strings.HasPrefix("forge:", name) { - // forge tags - keep = true - } else { - switch name { - // standard content tags - case "application-name", "author", "description", "keywords", "robots", "thumbnail": - keep = true - case "go-import", "go-source": - // golang tags - keep = true - case "apple-itunes-app": - } - } - - // prevent other arbitrary arguments - if keep { - newNode := html.Node{ - Type: html.ElementNode, - Data: n.Data, - } + var name string for _, attr := range n.Attr { if attr.Namespace != "" { continue } - if slices.Contains(safeAttributes, attr.Key) { - newNode.Attr = append(newNode.Attr, attr) + if attr.Key == "rel" { + name = attr.Val + break } } - if len(newNode.Attr) == 0 { + + if name == "" { continue } - entries = append(entries, newNode) + + var keep bool + if name == "icon" || name == "alternate icon" { + keep = true + } else if name == "alternate" || name == "canonical" || name == "search" { + // urls to versions of document + keep = true + } else if name == "author" || name == "privacy-policy" || name == "license" || name == "copyright" || name == "terms-of-service" { + keep = true + } else if name == "manifest" { + // web app manifest + keep = true + } + + // prevent other arbitrary arguments + if keep { + newNode := html.Node{ + Type: html.ElementNode, + Data: n.Data, + } + for _, attr := range n.Attr { + if attr.Namespace != "" { + continue + } + if slices.Contains(safeAttributes, attr.Key) { + newNode.Attr = append(newNode.Attr, attr) + } + } + if len(newNode.Attr) == 0 { + continue + } + entries = append(entries, newNode) + } + + case "meta": + + safeAttributes := []string{"name", "property", "content"} + var name string + for _, attr := range n.Attr { + if attr.Namespace != "" { + continue + } + if attr.Key == "name" { + name = attr.Val + break + } + if attr.Key == "property" && name == "" { + name = attr.Val + } + } + + if name == "" { + continue + } + + // prevent unwanted keys like CSRF and other internal entries to pass through as much as possible + + var keep bool + if strings.HasPrefix("og:", name) || strings.HasPrefix("fb:", name) || strings.HasPrefix("twitter:", name) || strings.HasPrefix("profile:", name) { + // social / OpenGraph tags + keep = true + } else if name == "vcs" || strings.HasPrefix("vcs:", name) { + // source tags + keep = true + } else if name == "forge" || strings.HasPrefix("forge:", name) { + // forge tags + keep = true + } else { + switch name { + // standard content tags + case "application-name", "author", "description", "keywords", "robots", "thumbnail": + keep = true + case "go-import", "go-source": + // golang tags + keep = true + case "apple-itunes-app": + } + } + + // prevent other arbitrary arguments + if keep { + newNode := html.Node{ + Type: html.ElementNode, + Data: n.Data, + } + for _, attr := range n.Attr { + if attr.Namespace != "" { + continue + } + if slices.Contains(safeAttributes, attr.Key) { + newNode.Attr = append(newNode.Attr, attr) + } + } + if len(newNode.Attr) == 0 { + continue + } + entries = append(entries, newNode) + } } + } state.tagCache.Set(key, entries, time.Hour*6) diff --git a/lib/template.go b/lib/template.go index c582b5a..dc73c2c 100644 --- a/lib/template.go +++ b/lib/template.go @@ -52,6 +52,28 @@ func initTemplate(name, data string) error { return nil } +func (state *State) addCachedTags(data *challenge.RequestData, r *http.Request, input map[string]any) { + proxyMetaTags := data.GetOptBool(challenge.RequestOptProxyMetaTags, false) + proxySafeLinkTags := data.GetOptBool(challenge.RequestOptProxySafeLinkTags, false) + if proxyMetaTags || proxySafeLinkTags { + backend, host := data.BackendHost() + if tags := state.fetchTags(host, backend, r, proxyMetaTags, proxySafeLinkTags); len(tags) > 0 { + metaTagMap, _ := input["MetaTags"].([]map[string]string) + linkTagMap, _ := input["LinkTags"].([]map[string]string) + + for _, tag := range tags { + tagAttrs := make(map[string]string, len(tag.Attr)) + for _, v := range tag.Attr { + tagAttrs[v.Key] = v.Val + } + metaTagMap = append(metaTagMap, tagAttrs) + } + input["MetaTags"] = metaTagMap + input["LinkTags"] = linkTagMap + } + } +} + func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status int, reg *challenge.Registration, params map[string]any) { data := challenge.RequestDataFromContext(r.Context()) input := make(map[string]any) @@ -75,21 +97,7 @@ func (state *State) ChallengePage(w http.ResponseWriter, r *http.Request, status input["Title"] = state.opt.Strings.Get("title_challenge") } - if data.GetOptBool(challenge.RequestOptCacheMetaTags, false) { - backend, host := data.BackendHost() - if tags := state.fetchMetaTags(host, backend, r); len(tags) > 0 { - tagMap, _ := input["Meta"].([]map[string]string) - - for _, tag := range tags { - tagAttrs := make(map[string]string, len(tag.Attr)) - for _, v := range tag.Attr { - tagAttrs[v.Key] = v.Val - } - tagMap = append(tagMap, tagAttrs) - } - input["Meta"] = tagMap - } - } + state.addCachedTags(data, r, input) w.Header().Set("Content-Type", "text/html; charset=utf-8") @@ -126,21 +134,7 @@ func (state *State) ErrorPage(w http.ResponseWriter, r *http.Request, status int input[k] = v } - if data.GetOptBool(challenge.RequestOptCacheMetaTags, false) { - backend, host := data.BackendHost() - if tags := state.fetchMetaTags(host, backend, r); len(tags) > 0 { - tagMap, _ := input["Meta"].([]map[string]string) - - for _, tag := range tags { - tagAttrs := make(map[string]string, len(tag.Attr)) - for _, v := range tag.Attr { - tagAttrs[v.Key] = v.Val - } - tagMap = append(tagMap, tagAttrs) - } - input["Meta"] = tagMap - } - } + state.addCachedTags(data, r, input) err2 := templates["challenge-"+state.opt.ChallengeTemplate+".gohtml"].Execute(buf, input) if err2 != nil { diff --git a/utils/tagfetcher.go b/utils/tagfetcher.go index 6ee90bd..00c59a0 100644 --- a/utils/tagfetcher.go +++ b/utils/tagfetcher.go @@ -6,9 +6,10 @@ import ( "net/http" "net/http/httptest" "net/url" + "slices" ) -func FetchTags(backend http.Handler, uri *url.URL, kind string) (result []html.Node) { +func FetchTags(backend http.Handler, uri *url.URL, kinds ...string) (result []html.Node) { writer := httptest.NewRecorder() backend.ServeHTTP(writer, &http.Request{ Method: http.MethodGet, @@ -39,7 +40,7 @@ func FetchTags(backend http.Handler, uri *url.URL, kind string) (result []html.N } for n := range node.Descendants() { - if n.Type == html.ElementNode && n.Data == kind { + if n.Type == html.ElementNode && slices.Contains(kinds, n.Data) { result = append(result, html.Node{ Type: n.Type, DataAtom: n.DataAtom,