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 Scripting
  • text/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: image

the name parameter is reflcted into the template Hello {{.}}

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: image1

name parameter 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:

  1. Code uses text/template somewhere, 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/template instead of text/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)
}

image2

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