html/template.Template în Go

Șabloane pentru HTML, cu aceeași interfață ca text/template, care codează automat datele după context (protecție XSS).

Sintaxă
import "html/template"

func New(name string) *Template
func (t *Template) Parse(text string) (*Template, error)
func (t *Template) Execute(wr io.Writer, data any) error

Ce face html/template.Template

html/template are aceleași funcții și aceeași sintaxă ca text/template (New, Parse, Execute, {{range}}, {{if}}…), dar înțelege HTML-ul. La Execute, fiecare valoare este codată potrivit locului în care apare: în text < devine &lt;, într-un atribut href se blochează adresele javascript:, într-un <script> valoarea devine un literal JavaScript sigur.

Asta te protejează de XSS (cross-site scripting): un comentariu care conține <script> apare pe pagină ca text, nu se execută. Dacă ai HTML de încredere pe care vrei să-l inserezi ca atare, îl marchezi explicit cu tipul template.HTML; nu face asta niciodată cu date de la utilizatori.

Regula: pentru orice ieșire HTML, importă html/template, nu text/template. Cele două pachete au același nume (template), deci verifică importul.

Ce returnează

La fel ca la text/template: Execute returnează o eroare.

Exemple

Exemplul 1: Comentarii afișate în siguranță

Scriptul din comentariu apare ca text. Adresa javascript: din href este înlocuită cu valoarea inofensivă #ZgotmplZ, iar & din adresa lui Ion devine &amp;, cum cere HTML-ul.

package main

import (
	"html/template"
	"os"
)

type Comentariu struct {
	Autor, Text, Site string
}

func main() {
	t := template.Must(template.New("comentarii").Parse(`<ul>
{{range .}}  <li><a href="{{.Site}}">{{.Autor}}</a>: {{.Text}}</li>
{{end}}</ul>
`))
	comentarii := []Comentariu{
		{"Ana", "Articol foarte util!", "https://ana.example.ro"},
		{"Atacator", `<script>alert("XSS")</script>`, `javascript:alert(1)`},
		{"Ion", `Prețul e < 100 & merită "luat"`, "https://ion.example.ro/?a=1&b=2"},
	}
	t.Execute(os.Stdout, comentarii)
}

Rezultat

<ul>
  <li><a href="https://ana.example.ro">Ana</a>: Articol foarte util!</li>
  <li><a href="#ZgotmplZ">Atacator</a>: &lt;script&gt;alert(&#34;XSS&#34;)&lt;/script&gt;</li>
  <li><a href="https://ion.example.ro/?a=1&amp;b=2">Ion</a>: Prețul e &lt; 100 &amp; merită &#34;luat&#34;</li>
</ul>

Exemplul 2: Aceeași valoare în contexte diferite

Valoarea este codată diferit în text, în atribut, în URL și în JavaScript. template.HTML marchează HTML de încredere, pus neschimbat.

package main

import (
	"html/template"
	"os"
)

func main() {
	t := template.Must(template.New("pagina").Parse(`<p title="{{.Nume}}">{{.Nume}}</p>
<a href="/cauta?q={{.Nume}}">caută</a>
<script>var nume = {{.Nume}};</script>
{{.Banner}}
`))
	t.Execute(os.Stdout, map[string]any{
		"Nume":   `Ana "Mica" & <Co>`,
		"Banner": template.HTML(`<strong>Reduceri -20%</strong>`),
	})
}

Rezultat

<p title="Ana &#34;Mica&#34; &amp; &lt;Co&gt;">Ana &#34;Mica&#34; &amp; &lt;Co&gt;</p>
<a href="/cauta?q=Ana%20%22Mica%22%20%26%20%3cCo%3e">caută</a>
<script>var nume = "Ana \"Mica\" \u0026 \u003cCo\u003e";</script>
<strong>Reduceri -20%</strong>

Exemplul 3: Pagină servită de un handler

package main

import (
	"bytes"
	"fmt"
	"html/template"
	"net/http"
	"net/http/httptest"
)

var pagina = template.Must(template.New("salut").Parse(
	`<h1>Salut, {{.}}!</h1>`))

func salut(w http.ResponseWriter, r *http.Request) {
	var buf bytes.Buffer
	if err := pagina.Execute(&buf, r.URL.Query().Get("nume")); err != nil {
		http.Error(w, "eroare internă", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	buf.WriteTo(w)
}

func main() {
	for _, q := range []string{"Maria", "<img src=x onerror=alert(1)>"} {
		rec := httptest.NewRecorder()
		req := httptest.NewRequest("GET", "/", nil)
		qv := req.URL.Query()
		qv.Set("nume", q)
		req.URL.RawQuery = qv.Encode()
		salut(rec, req)
		fmt.Println(rec.Body.String())
	}
}

Rezultat

<h1>Salut, Maria!</h1>
<h1>Salut, &lt;img src=x onerror=alert(1)&gt;!</h1>

Greșeli frecvente

  • Imporți text/template pentru o pagină web: datele nu sunt codate și pagina este vulnerabilă la XSS.
  • Convertești date de la utilizatori în template.HTML ca să „meargă formatarea”: anulezi protecția. Curăță HTML-ul cu o bibliotecă dedicată înainte, sau nu permite HTML.
  • Construiești HTML prin concatenare de texte în Go și îl dai șablonului: șablonul nu mai poate proteja ce nu vede ca date separate.

Comenzi înrudite

  • html/template.HTMLEscapeString: Codează caracterele speciale HTML (< > & ‘ „) dintr-un text, ca să poată fi pus sigur într-o pagină.
  • text/template.New: Creează un șablon text cu un nume; Parse îi încarcă textul cu acțiuni {{…}} care vor fi completate din date.
  • text/template.Template.Execute: Completează șablonul cu datele date și scrie rezultatul într-un io.Writer (ecran, fișier, răspuns HTTP, buffer).
  • net/http.ResponseWriter: Interfața prin care handler-ul construiește răspunsul: antete (Header), cod de stare (WriteHeader) și corp (Write).