Описание пакета html/template в Go

Пакет html/template - шаблоны HTML с защитой от инъекций

Пакет html/template реализует шаблоны, управляемые данными, для генерации HTML-вывода, защищённого от внедрения кода. Он предоставляет тот же интерфейс, что и text/template, и должен использоваться вместо него, когда выводом является HTML.

Данная документация фокусируется на функциях безопасности пакета. Информацию о программировании самих шаблонов см. в документации text/template.

Введение

Этот пакет оборачивает text/template, позволяя использовать его API шаблонов для безопасного разбора и выполнения HTML-шаблонов.

Пример:

tmpl, err := template.New("name").Parse(...)
// Проверка ошибок опущена
err = tmpl.Execute(out, data)

Если операция успешна, tmpl будет защищён от инъекций. В противном случае err будет содержать ошибку, как описано в ErrorCode.

HTML-шаблоны обрабатывают значения данных как обычный текст, который должен быть закодирован для безопасного встраивания в HTML-документ. Экранирование является контекстно-зависимым, поэтому действия могут появляться в JavaScript, CSS и URI-контекстах.

Модель безопасности, используемая этим пакетом, предполагает, что авторы шаблонов являются доверенными, а параметр данных Execute - нет. Более подробная информация приведена ниже.

import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

Вывод

Hello, <script>alert('you have been pwned')</script>!

но контекстное автоэкранирование в html/шаблоне

import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")

создает безопасный, экранированный HTML-вывод

Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!

Контексты

Этот пакет понимает HTML, CSS, JavaScript и URI. Он добавляет функции очистки к каждому простому конвейеру действий, поэтому, рассмотрим пример

<a href="/search?q={{.}}">{{.}}</a>

Во время разбора каждый {{.}} перезаписывается для добавления экранирующих функций по мере необходимости. В этом случае это становится

<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>

где urlescaper, attrescaper и htmlescaper — псевдонимы для внутренних функций экранирования.

Для этих внутренних функций экранирования, если конвейер действий оценивает значение интерфейса nil, он обрабатывается так, как будто это пустая строка.

Namespaced и data-атрибуты

Атрибуты с пространством имен обрабатываются так, как будто у них нет пространства имен. Рассмотрим пример:

<a my:href="{{.}}"></a>

Во время анализа атрибут будет обработан так, как если бы это был просто “href”. Таким образом, во время анализа шаблон становится:

<a my:href="{{. | urlescaper | attrescaper}}"></a>

Аналогично атрибутам с пространствами имен, атрибуты с префиксом “data-” обрабатываются так, как если бы у них не было префикса “data-”. Рассмотрим пример:

<a data-href="{{.}}"></a>

Во время анализа это становится

<a data-href="{{. | urlescaper | attrescaper}}"></a>

Если атрибут имеет как пространство имен, так и префикс “data-”, то при определении контекста будет удалено только пространство имен. Например:

<a my:data-href="{{.}}"></a>

Это обрабатывается так, как если бы “my:data-href” было просто “data-href”, а не “href”, как было бы, если бы префикс “data-” тоже игнорировался. Таким образом, во время анализа это становится просто

<a my:data-href="{{. | attrescaper}}"></a>

В качестве особого случая атрибуты с пространством имен “xmlns” всегда рассматриваются как содержащие URL-адреса. Например:

<a xmlns:title="{{.}}"></a>
<a xmlns:href="{{.}}"></a>
<a xmlns:onclick="{{.}}"></a>

Во время анализа они становятся:

<a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>

Ошибки

Подробности см. в документации ErrorCode.

Более полная картина

Остальную часть этого комментария к пакету можно пропустить при первом чтении; он включает детали, необходимые для понимания экранирования контекстов и сообщений об ошибках. Большинству пользователей не нужно понимать эти детали.

Контексты

Предположив, что {{.}} — это O'Reilly: How are <i>you</i>?, в таблице ниже показано, как выглядит {{.}} при использовании в контексте слева.

Context                          {{.}} After
{{.}}                            O'Reilly: How are &lt;i&gt;you&lt;/i&gt;?
<a title='{{.}}'>                O&#39;Reilly: How are you?
<a href="/{{.}}">                O&#39;Reilly: How are %3ci%3eyou%3c/i%3e?
<a href="?q={{.}}">              O&#39;Reilly%3a%20How%20are%3ci%3e...%3f
<a onx='f("{{.}}")'>             O\x27Reilly: How are \x3ci\x3eyou...?
<a onx='f({{.}})'>               "O\x27Reilly: How are \x3ci\x3eyou...?"
<a onx='pattern = /{{.}}/;'>     O\x27Reilly: How are \x3ci\x3eyou...\x3f

При использовании в небезопасном контексте значение может быть отфильтровано:

Context                          {{.}} After
<a href="{{.}}">                 #ZgotmplZ

поскольку “O’Reilly:” не является разрешенным протоколом, как “http:”.

Если {{.}} — это безобидное слово left, то оно может появляться более широко,

Context                              {{.}} After
{{.}}                                left
<a title='{{.}}'>                    left
<a href='{{.}}'>                     left
<a href='/{{.}}'>                    left
<a href='?dir={{.}}'>                left
<a style="border-{{.}}: 4px">        left
<a style="align: {{.}}">             left
<a style="background: '{{.}}'>       left
<a style="background: url('{{.}}')>  left
<style>p.{{.}} {color:red}</style>   left

Нестроковые значения могут использоваться в контекстах JavaScript. Если {{.}} — это

struct{A,B string}{ "foo", "bar" }

в экранированном шаблоне

<script>var pair = {{.}};</script>

то вывод шаблона будет

<script>var pair = {"A": "foo", "B": "bar"};</script>

Ознакомьтесь с пакетом json, чтобы понять, как нестроковый контент маршалируется для встраивания в контексты JavaScript.

Типизированные строки

По умолчанию этот пакет предполагает, что все конвейеры производят простую текстовую строку. Он добавляет этапы экранирования конвейера, необходимые для корректного и безопасного встраивания этой простой текстовой строки в соответствующий контекст.

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

Типы HTML, JS, URL и другие из content.go могут содержать безопасный контент, который освобожден от экранирования.

Например шаблон:

Hello, {{.}}!

может быть вызван с помощью

tmpl.Execute(out, template.HTML(`<b>World</b>`))

вывод

Hello, <b>World</b>!

вместо

Hello, &lt;b&gt;World&lt;b&gt;!

который был бы получен, если бы {{.}} был обычной строкой.

Модель безопасности

Определение “безопасности”, используемое в этом пакете, основано на спецификации.

Данный пакет исходит из следующих допущений:

  • Авторы шаблонов считаются доверенными
  • Данные, передаваемые в Execute, — ненадёжные

При этом обеспечиваются следующие свойства безопасности при работе с ненадёжными данными:

1. Сохранение структуры (Structure Preservation Property)

Если автор шаблона пишет HTML-тег на безопасном языке шаблонов, браузер всегда будет интерпретировать соответствующую часть вывода именно как тег — независимо от значений ненадёжных данных. То же самое относится к другим структурам, таким как границы атрибутов, строки JavaScript и CSS.

2. Контроль исполнения кода (Code Effect Property)

В результате вставки вывода шаблона на страницу должен выполняться только код, явно указанный автором шаблона. При этом весь код, указанный автором, должен выполняться корректно.

3. Принцип наименьшего удивления (Least Surprise Property)

Разработчик (или ревьюер кода), знакомый с HTML, CSS и JavaScript и знающий о контекстном автоэкранировании, должен быть способен, увидев {{.}}, однозначно определить, какое экранирование будет применено.

Поддержка шаблонных литералов ES6

Ранее шаблонные литералы ECMAScript 6 (${...}) были отключены по умолчанию, и их можно было включить через переменную окружения GODEBUG=jstmpllitinterp=1.

Сейчас шаблонные литералы поддерживаются по умолчанию, а настройка jstmpllitinterp больше не имеет эффекта.

Пример
package main

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

func main() {
	const tpl = `
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>{{.Title}}</title>
	</head>
	<body>
		{{range .Items}}<div>{{ . }}</div>{{else}}<div><strong>no rows</strong></div>{{end}}
	</body>
</html>`

	check := func(err error) {
		if err != nil {
			log.Fatal(err)
		}
	}
	t, err := template.New("webpage").Parse(tpl)
	check(err)

	data := struct {
		Title string
		Items []string
	}{
		Title: "My page",
		Items: []string{
			"My photos",
			"My blog",
		},
	}

	err = t.Execute(os.Stdout, data)
	check(err)

	noItems := struct {
		Title string
		Items []string
	}{
		Title: "My another page",
		Items: []string{},
	}

	err = t.Execute(os.Stdout, noItems)
	check(err)

}
Output:

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>My page</title>
	</head>
	<body>
		<div>My photos</div><div>My blog</div>
	</body>
</html>
<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title>My another page</title>
	</head>
	<body>
		<div><strong>no rows</strong></div>
	</body>
</html>
Пример Autoescaping
package main

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

func main() {
	check := func(err error) {
		if err != nil {
			log.Fatal(err)
		}
	}
	t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
	check(err)
	err = t.ExecuteTemplate(os.Stdout, "T", "<script>alert('you have been pwned')</script>")
	check(err)
}
Output:

Hello, &lt;script&gt;alert(&#39;you have been pwned&#39;)&lt;/script&gt;!
Пример Escape
package main

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

func main() {
	const s = `"Fran & Freddie's Diner" <tasty@example.com>`
	v := []any{`"Fran & Freddie's Diner"`, ' ', `<tasty@example.com>`}

	fmt.Println(template.HTMLEscapeString(s))
	template.HTMLEscape(os.Stdout, []byte(s))
	fmt.Fprintln(os.Stdout, "")
	fmt.Println(template.HTMLEscaper(v...))

	fmt.Println(template.JSEscapeString(s))
	template.JSEscape(os.Stdout, []byte(s))
	fmt.Fprintln(os.Stdout, "")
	fmt.Println(template.JSEscaper(v...))

	fmt.Println(template.URLQueryEscaper(v...))

}
Output:

&#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;tasty@example.com&gt;
&#34;Fran &amp; Freddie&#39;s Diner&#34; &lt;tasty@example.com&gt;
&#34;Fran &amp; Freddie&#39;s Diner&#34;32&lt;tasty@example.com&gt;
\"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
\"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
\"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
%22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E