For this blog, we’ll look at CVE-2026-34530: Stored Cross-Site Scripting via text/template branding injection and trace it back to the source code to understand exactly where and why the vulnerability exists.
We have two keyword in the title:
Stored Cross-Site Scriptingtext/template branding injection
The blog assumes you at least have a basic understanding of Cross-Site Scripting. You may learn more about it at What is cross-site scripting (XSS)?
We’ll come down to the second keyword: text/template branding injection
In Go lang, we have a package text/template which is used when we want to create text dynamically by putting variables/data into a predefined template. Eg:

the
nameparameter is reflcted into the templateHello {{.}}
package main
import (
"net/http"
"html/template"
)
func main(){
t:=template.Must(
template.New("hello").Parse(
`
<html>
<body>Hello {{.}}</body>
</html>
`,
),
)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
name:=r.URL.Query().Get("name")
t.Execute(w, name)
})
http.ListenAndServe(":8080", nil)
}
The problem is text/template (by-default) doesn’t HTML-escape template data, meaning if the parameter is attacker controlled, they can simply inject javascript. Eg:

nameparameter is injected with JS<script>alert(1)</script>
Coming back to the title, Stored Cross-Site Scripting via text/template branding injection, we can actually guess what’s happening here:
- Code uses
text/templatesomewhere, and directy pass it to the template triggering XSS.
The vulnerable code:
http/static.go in version <= 2.62.1 uses text/template, and passes the branding.Name value directly into template without escaping.
// http/static.go, line 33 — branding.Name passed into template data
"Name": d.settings.Branding.Name,
Here, d is the application’s internal data context. d.settings.Branding.Name retrieves the configured branding name and stores it in the template data map under the Name key.
The template is then parsed using text/template:
// http/static.go, line 97 — template parsed with custom delimiters, no escaping
index := template.Must(template.New("index").Delims("[{[", "]}]").Parse(string(fileContents)))
Finally, that data is rendered into the HTTP response:
index.Execute(w, data)
And the frontend template (frontend/public/index.html) embeds these fields directly:
<!-- frontend/public/index.html, line 16 -->
[{[ if .Name -]}][{[ .Name ]}][{[ else ]}]File Browser[{[ end ]}]
As per the report, the branding name is rendered inside the <title> element. Since text/template doesn’t escape html, a PoC can be: </title><script>alert(1)</script>, breaking the <title>, and triggering the JS.
There can be 2 option to fix this:
- Either use
text/template+html.EscapeString(userInput)to escape the HTML - Or, directly use
html/templateinstead oftext/template. Eg:
package main
import (
"net/http"
"html/template"
)
func main(){
t:=template.Must(
template.New("hello").Parse(
`
<html>
<body>Hello {{.}}</body>
</html>
`,
),
)
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request){
name:=r.URL.Query().Get("name")
t.Execute(w, name)
})
http.ListenAndServe(":8080", nil)
}

The FileBrowser team has used the html/template option to fix the vulnerability, as it’s more context awareness rather than simply, “HTML escape everything”.
For example:
<div>{{.Name}}</div>requires HTML escape.- But,
<a href="{{.URL}}">Link</a>has a URL context, which has a different security requirement.
So, html/template handles everything on it’s own, making it much more easier.
Ref: https://github.com/filebrowser/filebrowser/commit/d9f9460c1e51d10a25065e10358c12d5ced66ad9
