state context: Added proxy-safe-link-tags to proxy <link> tags, use specific LinkTags ranger on templates instead of raw elements

This commit is contained in:
WeebDataHoarder
2025-05-03 04:00:17 +02:00
parent 736c2708e9
commit 1ea19c5a6c
9 changed files with 163 additions and 92 deletions

View File

@@ -5,9 +5,12 @@
<link rel="stylesheet" href="{{ .Path }}/assets/static/anubis/style.css?cacheBust={{ .Random }}"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="referrer" content="origin"/>
{{ range .Meta }}
{{ range .MetaTags }}
<meta {{ range $key, $value := . }}{{ $key | attr }}="{{ $value }}" {{end}}/>
{{ end }}
{{ range .LinkTags }}
<link {{ range $key, $value := . }}{{ $key | attr }}="{{ $value }}" {{end}}/>
{{ end }}
{{ range .HeaderTags }}
{{ . }}
{{ end }}

View File

@@ -6,9 +6,12 @@
<title>{{ .Title }}</title>
<meta name="referrer" content="origin">
{{ range .Meta }}
{{ range .MetaTags }}
<meta {{ range $key, $value := . }}{{ $key | attr }}="{{ $value }}" {{end}}/>
{{ end }}
{{ range .LinkTags }}
<link {{ range $key, $value := . }}{{ $key | attr }}="{{ $value }}" {{end}}/>
{{ end }}
{{ range .HeaderTags }}
{{ . }}
{{ end }}

View File

@@ -293,6 +293,7 @@ rules:
context-set:
# Map OpenGraph or similar <meta> tags back to the reply, even if denied/challenged
proxy-meta-tags: "true"
# proxy-safe-link-tags: "true"
# Set additional response headers
#response-headers:

View File

@@ -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) {

View File

@@ -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(),

View File

@@ -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("<link href=\"%s\" rel=\"stylesheet\" crossorigin=\"use-credentials\">", uri.String())),
"LinkTags": []map[string]string{
{
"href": uri.String(),
"rel": "stylesheet",
"crossorigin": "use-credentials",
},
},
})
return challenge.VerifyResultNone

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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,