Типы экранирования HTML/JavaScript/URL пакета html/template языка Go

Описание типов и функций пакета html/template языка Go в соответствии с официальной документацией

type CSS

type CSS string

CSS инкапсулирует заранее проверенное содержимое, соответствующее:

  • Производству CSS3 таблиц стилей (напр. p { color: purple })
  • Производству CSS3 правил (напр. a[href=~"https:"].foo#bar)
  • Производству CSS3 деклараций (напр. color: red; margin: 2px)
  • Производству CSS3 значений (напр. rgba(0, 0, 255, 127))

Внимание: Использование этого типа представляет угрозу безопасности - содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.

type Error

type Error struct {
    ErrorCode    ErrorCode  // Тип ошибки
    Node         parse.Node // Узел, вызвавший проблему (если известен)
    Name         string     // Имя шаблона с ошибкой
    Line         int        // Номер строки в исходнике шаблона
    Description  string     // Человекочитаемое описание
}

Описывает проблему, возникшую при экранировании шаблона.

Коды ошибок (ErrorCode)

const (
    OK ErrorCode = iota  // Нет ошибки
    
    // Неоднозначный контекст в URL
    ErrAmbigContext
    
    // Некорректный HTML (незакрытые теги/атрибуты)
    ErrBadHTML
    
    // Ветки if/range заканчиваются в разных контекстах
    ErrBranchEnd
    
    // Шаблон заканчивается в нетекстовом контексте
    ErrEndContext
    
    // Отсутствует указанный шаблон
    ErrNoSuchTemplate
    
    // Невозможно вычислить контекст вывода
    ErrOutputContext
    
    // Незавершённый набор символов в regexp JS
    ErrPartialCharset
    
    // Незавершённая escape-последовательность
    ErrPartialEscape
    
    // Повторный вход в range с изменением контекста
    ErrRangeLoopReentry
    
    // Неоднозначная интерпретация '/' (деление или regexp)
    ErrSlashAmbig
    
    // Использование запрещённых экранировщиков
    ErrPredefinedEscaper
    
    // Устаревшая ошибка для JS template literals
    ErrJSTemplate
)

Особое значение ZgotmplZ

Пример:

<img src="{{.X}}"> 

где {{.X}} вычисляется в javascript:...

Результат:

<img src="#ZgotmplZ">

Объяснение: “ZgotmplZ” - специальное значение, указывающее, что небезопасное содержимое достигло CSS или URL контекста во время выполнения. Для доверенных источников используйте типы содержимого для отключения фильтрации: URL(javascript:…).

type FuncMap

type FuncMap = template.FuncMap

FuncMap - это псевдоним для типа template.FuncMap из пакета text/template, представляющий карту функций для использования в шаблонах.

type HTML

type HTML string

HTML инкапсулирует безопасный фрагмент HTML-документа. Не следует использовать для:

  • HTML из ненадёжных источников
  • HTML с незакрытыми тегами или комментариями

Внимание: Содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.

type HTMLAttr

type HTMLAttr string

HTMLAttr инкапсулирует безопасный HTML-атрибут, например dir="ltr".

Внимание: Как и с HTML, содержимое должно быть доверенным, так как включается без экранирования.

type JS

type JS string

JS инкапсулирует безопасное выражение EcmaScript5, например (x + y * z()).

Особенности:

  • Авторы шаблонов должны обеспечивать правильный порядок операций
  • Не должно быть неоднозначности между выражениями и инструкциями

Внимание: Не безопасно использовать для непроверенного JSON. Для JSON лучше использовать json.Unmarshal.

type JSStr

type JSStr string

JSStr инкапсулирует безопасную строку для JavaScript, которая будет заключена в кавычки.

Требования:

  • Допустимые символы или escape-последовательности
  • Запрещены переносы строк внутри escape-последовательностей

Пример:

JSStr("foo\\nbar")  // допустимо
JSStr("foo\\\nbar") // недопустимо

type Srcset

type Srcset string

Srcset инкапсулирует безопасный атрибут srcset для тегов <img> (спецификация HTML5).

type Template

type Template struct {
    Tree *parse.Tree  // Абстрактное синтаксическое дерево шаблона
    // ... скрытые поля
}

Template - это специализированная версия text/template, которая генерирует безопасные фрагменты HTML-документов.

Ключевые особенности:

  • Расширяет базовый функционал text/template
  • Автоматически применяет контекстно-зависимое экранирование
  • Гарантирует безопасность вывода при правильном использовании

Все типы безопасного контента (HTML, JS и др.) требуют, чтобы их содержимое поступало из доверенных источников, так как они включаются в вывод без дополнительного экранирования.

Пример Block
package main

import (
	"html/template"
	"log"
	"os"
	"strings"
)

func main() {
	const (
		master  = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
		overlay = `{{define "list"}} {{join . ", "}}{{end}} `
	)
	var (
		funcs     = template.FuncMap{"join": strings.Join}
		guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
	)
	masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
	if err != nil {
		log.Fatal(err)
	}
	overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
	if err != nil {
		log.Fatal(err)
	}
	if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
		log.Fatal(err)
	}
	if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
		log.Fatal(err)
	}
}
Output:

Names:
- Gamora
- Groot
- Nebula
- Rocket
- Star-Lord
Names: Gamora, Groot, Nebula, Rocket, Star-Lord
Пример Glob

Здесь мы демонстрируем загрузку набора шаблонов из каталога.

package main

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"text/template"
)

// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
	name     string
	contents string
}

func createTestDir(files []templateFile) string {
	dir, err := os.MkdirTemp("", "template")
	if err != nil {
		log.Fatal(err)
	}
	for _, file := range files {
		f, err := os.Create(filepath.Join(dir, file.name))
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		_, err = io.WriteString(f, file.contents)
		if err != nil {
			log.Fatal(err)
		}
	}
	return dir
}

func main() {
	// Here we create a temporary directory and populate it with our sample
	// template definition files; usually the template files would already
	// exist in some location known to the program.
	dir := createTestDir([]templateFile{
		// T0.tmpl is a plain template file that just invokes T1.
		{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
		// T1.tmpl defines a template, T1 that invokes T2.
		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
		// T2.tmpl defines a template T2.
		{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
	})
	// Clean up after the test; another quirk of running as an example.
	defer os.RemoveAll(dir)

	// pattern is the glob pattern used to find all the template files.
	pattern := filepath.Join(dir, "*.tmpl")

	// Here starts the example proper.
	// T0.tmpl is the first name matched, so it becomes the starting template,
	// the value returned by ParseGlob.
	tmpl := template.Must(template.ParseGlob(pattern))

	err := tmpl.Execute(os.Stdout, nil)
	if err != nil {
		log.Fatalf("template execution: %s", err)
	}
}
Output:

T0 invokes T1: (T1 invokes T2: (This is T2))
Пример Helpers

Этот пример демонстрирует один из способов поделиться некоторыми шаблонами и использовать их в разных контекстах. В этом варианте мы вручную добавляем несколько шаблонов драйверов в существующий пакет шаблонов.

package main

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"text/template"
)

// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
	name     string
	contents string
}

func createTestDir(files []templateFile) string {
	dir, err := os.MkdirTemp("", "template")
	if err != nil {
		log.Fatal(err)
	}
	for _, file := range files {
		f, err := os.Create(filepath.Join(dir, file.name))
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		_, err = io.WriteString(f, file.contents)
		if err != nil {
			log.Fatal(err)
		}
	}
	return dir
}

func main() {
	// Here we create a temporary directory and populate it with our sample
	// template definition files; usually the template files would already
	// exist in some location known to the program.
	dir := createTestDir([]templateFile{
		// T1.tmpl defines a template, T1 that invokes T2.
		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
		// T2.tmpl defines a template T2.
		{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
	})
	// Clean up after the test; another quirk of running as an example.
	defer os.RemoveAll(dir)

	// pattern is the glob pattern used to find all the template files.
	pattern := filepath.Join(dir, "*.tmpl")

	// Here starts the example proper.
	// Load the helpers.
	templates := template.Must(template.ParseGlob(pattern))
	// Add one driver template to the bunch; we do this with an explicit template definition.
	_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
	if err != nil {
		log.Fatal("parsing driver1: ", err)
	}
	// Add another driver template.
	_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
	if err != nil {
		log.Fatal("parsing driver2: ", err)
	}
	// We load all the templates before execution. This package does not require
	// that behavior but html/template's escaping does, so it's a good habit.
	err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
	if err != nil {
		log.Fatalf("driver1 execution: %s", err)
	}
	err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
	if err != nil {
		log.Fatalf("driver2 execution: %s", err)
	}
}
Output:

Driver 1 calls T1: (T1 invokes T2: (This is T2))
Driver 2 calls T2: (This is T2)
Пример Parsefiles

Здесь мы демонстрируем загрузку набора шаблонов из файлов в разных каталогах.

package main

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"text/template"
)

// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
	name     string
	contents string
}

func createTestDir(files []templateFile) string {
	dir, err := os.MkdirTemp("", "template")
	if err != nil {
		log.Fatal(err)
	}
	for _, file := range files {
		f, err := os.Create(filepath.Join(dir, file.name))
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		_, err = io.WriteString(f, file.contents)
		if err != nil {
			log.Fatal(err)
		}
	}
	return dir
}

func main() {
	// Here we create different temporary directories and populate them with our sample
	// template definition files; usually the template files would already
	// exist in some location known to the program.
	dir1 := createTestDir([]templateFile{
		// T1.tmpl is a plain template file that just invokes T2.
		{"T1.tmpl", `T1 invokes T2: ({{template "T2"}})`},
	})

	dir2 := createTestDir([]templateFile{
		// T2.tmpl defines a template T2.
		{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
	})

	// Clean up after the test; another quirk of running as an example.
	defer func(dirs ...string) {
		for _, dir := range dirs {
			os.RemoveAll(dir)
		}
	}(dir1, dir2)

	// Here starts the example proper.
	// Let's just parse only dir1/T0 and dir2/T2
	paths := []string{
		filepath.Join(dir1, "T1.tmpl"),
		filepath.Join(dir2, "T2.tmpl"),
	}
	tmpl := template.Must(template.ParseFiles(paths...))

	err := tmpl.Execute(os.Stdout, nil)
	if err != nil {
		log.Fatalf("template execution: %s", err)
	}
}
Output:

T1 invokes T2: (This is T2)
Пример Share

В этом примере показано, как использовать одну группу шаблонов драйверов с различными наборами вспомогательных шаблонов.

package main

import (
	"io"
	"log"
	"os"
	"path/filepath"
	"text/template"
)

// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
	name     string
	contents string
}

func createTestDir(files []templateFile) string {
	dir, err := os.MkdirTemp("", "template")
	if err != nil {
		log.Fatal(err)
	}
	for _, file := range files {
		f, err := os.Create(filepath.Join(dir, file.name))
		if err != nil {
			log.Fatal(err)
		}
		defer f.Close()
		_, err = io.WriteString(f, file.contents)
		if err != nil {
			log.Fatal(err)
		}
	}
	return dir
}

func main() {
	// Here we create a temporary directory and populate it with our sample
	// template definition files; usually the template files would already
	// exist in some location known to the program.
	dir := createTestDir([]templateFile{
		// T0.tmpl is a plain template file that just invokes T1.
		{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
		// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
		{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
	})
	// Clean up after the test; another quirk of running as an example.
	defer os.RemoveAll(dir)

	// pattern is the glob pattern used to find all the template files.
	pattern := filepath.Join(dir, "*.tmpl")

	// Here starts the example proper.
	// Load the drivers.
	drivers := template.Must(template.ParseGlob(pattern))

	// We must define an implementation of the T2 template. First we clone
	// the drivers, then add a definition of T2 to the template name space.

	// 1. Clone the helper set to create a new name space from which to run them.
	first, err := drivers.Clone()
	if err != nil {
		log.Fatal("cloning helpers: ", err)
	}
	// 2. Define T2, version A, and parse it.
	_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
	if err != nil {
		log.Fatal("parsing T2: ", err)
	}

	// Now repeat the whole thing, using a different version of T2.
	// 1. Clone the drivers.
	second, err := drivers.Clone()
	if err != nil {
		log.Fatal("cloning drivers: ", err)
	}
	// 2. Define T2, version B, and parse it.
	_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
	if err != nil {
		log.Fatal("parsing T2: ", err)
	}

	// Execute the templates in the reverse order to verify the
	// first is unaffected by the second.
	err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
	if err != nil {
		log.Fatalf("second execution: %s", err)
	}
	err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
	if err != nil {
		log.Fatalf("first: execution: %s", err)
	}

}
Output:

T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))

func Must

func Must(t *Template, err error) *Template

Must - вспомогательная функция, которая оборачивает вызов функции, возвращающей (*Template, error), и вызывает панику, если ошибка не равна nil. Предназначена для инициализации переменных:

Пример:

var t = template.Must(template.New("name").Parse("html"))

func New

func New(name string) *Template

New создает новый HTML-шаблон с указанным именем.

func ParseFS (добавлена в Go 1.16)

func ParseFS(fs fs.FS, patterns ...string) (*Template, error)

ParseFS аналогична ParseFiles и ParseGlob, но читает из файловой системы fs вместо файловой системы хоста. Принимает список glob-шаблонов (большинство имен файлов работают как glob-шаблоны, соответствующие только самим себе).

func ParseFiles

func ParseFiles(filenames ...string) (*Template, error)

ParseFiles создает новый шаблон и парсит определения шаблонов из указанных файлов. Имя возвращаемого шаблона будет соответствовать базовому имени первого файла. Должен быть указан хотя бы один файл.

Особенности:

  • При парсинге нескольких файлов с одинаковыми именами будет использован последний указанный файл
  • Например, ParseFiles("a/foo", "b/foo") сохранит “b/foo” как шаблон “foo”, а “a/foo” будет недоступен

func ParseGlob

func ParseGlob(pattern string) (*Template, error)

ParseGlob создает новый шаблон и парсит определения шаблонов из файлов, соответствующих указанному шаблону (согласно семантике filepath.Match). Шаблон должен соответствовать хотя бы одному файлу.

Эквивалентно вызову ParseFiles со списком файлов, соответствующих шаблону. Как и в ParseFiles, при дублировании имен используется последний файл.

func (*Template) AddParseTree

func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)

AddParseTree создает новый шаблон с указанным именем и деревом разбора, ассоциируя его с t.

Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены.

func (*Template) Clone

func (t *Template) Clone() (*Template, error)

Clone создает копию шаблона, включая все ассоциированные шаблоны. Последующие вызовы Parse в копии будут добавлять шаблоны только в копию.

Возвращает ошибку, если t уже был выполнен.

func (*Template) DefinedTemplates (добавлена в Go 1.6)

func (t *Template) DefinedTemplates() string

DefinedTemplates возвращает строку с перечислением определенных шаблонов с префиксом “; defined templates are: “. Если шаблонов нет, возвращает пустую строку. Используется для генерации сообщений об ошибках.

func (*Template) Delims

func (t *Template) Delims(left, right string) *Template

Delims устанавливает разделители действий для последующих вызовов Parse, ParseFiles или ParseGlob. Вложенные определения шаблонов наследуют эти настройки. Пустые разделители соответствуют значениям по умолчанию: {{ и }}.

Возвращает сам шаблон, позволяя объединять вызовы в цепочку.

Пример
package main

import (
	"html/template"
	"log"
	"os"
)

func main() {
	const text = "<<.Greeting>> {{.Name}}"

	data := struct {
		Greeting string
		Name     string
	}{
		Greeting: "Hello",
		Name:     "Joe",
	}

	t := template.Must(template.New("tpl").Delims("<<", ">>").Parse(text))

	err := t.Execute(os.Stdout, data)
	if err != nil {
		log.Fatal(err)
	}

}
Output:

Hello {{.Name}}

func (*Template) Execute

func (t *Template) Execute(wr io.Writer, data any) error

Применяет разобранный шаблон к указанным данным, записывая результат в wr. В случае ошибки выполнение прерывается, но частичные результаты могут быть уже записаны. Шаблон можно выполнять параллельно, но при использовании общего Writer вывод может перемешиваться.

func (*Template) ExecuteTemplate

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

Применяет шаблон с указанным именем, ассоциированный с t, к данным и записывает результат в wr. Поведение при ошибках и параллельном выполнении аналогично Execute.

func (*Template) Funcs

func (t *Template) Funcs(funcMap FuncMap) *Template

Добавляет функции из переданной мапы в функциональную мапу шаблона. Должен вызываться до парсинга шаблона. Вызывает панику, если значение в мапе не является функцией с подходящим типом возвращаемого значения. Возвращает сам шаблон для цепочки вызовов.

func (*Template) Lookup

func (t *Template) Lookup(name string) *Template

Возвращает шаблон с указанным именем, ассоциированный с t, или nil если такого шаблона нет.

func (*Template) Name

func (t *Template) Name() string

Возвращает имя шаблона.

func (*Template) New

func (t *Template) New(name string) *Template

Создает новый HTML-шаблон с указанным именем, ассоциированный с текущим и имеющий те же разделители. Позволяет использовать вызовы шаблонов через {{template}}. Если шаблон с таким именем уже существует, он будет заменен.

func (*Template) Option

func (t *Template) Option(opt ...string) *Template

Устанавливает опции шаблона в формате строк “ключ=значение” или простых строк. Вызывает панику при неизвестной или неверной опции.

Поддерживаемые опции:

  • missingkey=default или missingkey=invalid - поведение по умолчанию (продолжать выполнение)
  • missingkey=zero - возвращать нулевое значение для отсутствующего ключа
  • missingkey=error - прерывать выполнение с ошибкой

func (*Template) Parse

func (t *Template) Parse(text string) (*Template, error)

Разбирает текст как тело шаблона для t. Определения именованных шаблонов ({{define}}/{{block}}) становятся ассоциированными шаблонами. Шаблоны можно переопределять до первого вызова Execute. Пустые определения (только пробелы/комментарии) не заменяют существующие шаблоны.

func (*Template) ParseFS

func (t *Template) ParseFS(fs fs.FS, patterns ...string) (*Template, error)

ParseFS аналогична Template.ParseFiles и Template.ParseGlob, но читает из абстрактной файловой системы fs вместо файловой системы хоста. Принимает список glob-шаблонов (большинство имен файлов работают как glob-шаблоны, соответствующие только самим себе).

func (*Template) ParseFiles

func (t *Template) ParseFiles(filenames ...string) (*Template, error)

ParseFiles парсит указанные файлы и ассоциирует полученные шаблоны с t. При ошибке парсинг останавливается и возвращается nil, иначе возвращается t. Должен быть указан хотя бы один файл.

Особенности:

  • При парсинге нескольких файлов с одинаковыми именами используется последний указанный файл
  • Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены

func (*Template) ParseGlob

func (t *Template) ParseGlob(pattern string) (*Template, error)

ParseGlob парсит шаблоны из файлов, соответствующих указанному шаблону (согласно семантике filepath.Match), и ассоциирует их с t. Эквивалентно вызову t.ParseFiles со списком файлов, соответствующих шаблону.

Особенности:

  • Как и в ParseFiles, при дублировании имен используется последний файл
  • Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены

func (*Template) Templates

func (t *Template) Templates() []*Template

Templates возвращает срез всех шаблонов, ассоциированных с t, включая сам t.

type URL

type URL string

URL инкапсулирует безопасный URL или подстроку URL (согласно RFC 3986). URL типа javascript:checkThatFormNotEditedBeforeLeavingPage() из доверенного источника может быть использован, но по умолчанию динамические javascript: URL фильтруются как частый вектор атак.

Внимание: Использование этого типа представляет угрозу безопасности - содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.