Serangan XSS (cross-site scripting) lahir dari satu celah sederhana: data pengguna masuk ke halaman tanpa disaring, lalu dibaca browser sebagai kode. Mengamankannya bukan sekadar mengganti < menjadi &lt; — perlakuan yang benar berbeda di tiap posisi markup: teks, atribut, CSS, JavaScript, dan URL punya aturan main sendiri. Kabar baiknya, html/template mengerjakan analisis tersebut secara otomatis dan gratis. Chapter ini membongkar mesin tersebut: kita akan meletakkan satu payload berbahaya yang sama pada lima konteks berbeda, mengamati lima hasil escaping yang berbeda, lalu mengenal tipe penanda tepercaya sebagai pintu darurat yang terkontrol.

Auto-Escape yang Sadar Konteks #

Ketika html/template mengurai string template, package tersebut tidak hanya mem-parse aksi {{ }} — ia juga menokenisasi markup HTML di sekelilingnya dan melacak posisi setiap aksi dalam sebuah mesin status konteks. Aksi di antara tag dikenali sebagai konteks teks; aksi di dalam href="..." dikenali sebagai konteks URL di dalam atribut; aksi di dalam elemen <script> dikenali sebagai konteks JavaScript. Keputusan escaper mana yang dipasang diambil sepenuhnya saat parsing, sehingga proses eksekusi tetap cepat dan hasilnya konsisten.

Program Utuh: Satu Payload, Lima Konteks #

Berkas escaping-demo.go merender string <script>alert('xss')</script> — payload XSS klasik — pada lima posisi berbeda, lalu menguji penyaringan skema URL dan tipe penanda tepercaya:

go
package main

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

// render mengeksekusi template dan mengembalikan string hasilnya.
func render(t *template.Template, data any) string {
	var buf bytes.Buffer
	if err := t.Execute(&buf, data); err != nil {
		panic(err)
	}
	return buf.String()
}

func main() {
	// Payload XSS yang sama ditempatkan pada lima konteks berbeda.
	const lima = `1. body: <p>{{.}}</p>
2. attr: <p data-info="{{.}}">isi</p>
3. css : <p style="color: {{.}}">isi</p>
4. js  : <script>var payload = {{.}};</script>
5. url : <a href="{{.}}">isi</a>
`

	t := template.Must(template.New("lima").Parse(lima))
	payload := `<script>alert('xss')</script>`

	fmt.Println("=== payload yang sama, lima konteks ===")
	fmt.Println(render(t, payload))

	// Skema URL berbahaya disaring menjadi #ZgotmplZ.
	fmt.Println("=== skema javascript: pada atribut URL ===")
	tTautan := template.Must(template.New("tautan").Parse(`<a href="{{.}}">klik</a>`))
	fmt.Println(render(tTautan, "javascript:alert(1)"))

	// Tipe penanda tepercaya mematikan pengaman pada konteksnya.
	fmt.Println("=== tipe tepercaya: HTML, JS, dan URL ===")
	const raw = `aman: {{.Markup}}
konfigurasi: <script>var k = {{.Konfig}};</script>
tautan: <a href="{{.Tautan}}">buka</a>`
	tRaw := template.Must(template.New("raw").Parse(raw))
	data := map[string]any{
		"Markup": template.HTML(`<b>markup dari editor tepercaya</b>`),
		"Konfig": template.JS(`{versi: 2}`),
		"Tautan": template.URL(`https://example.com/?q=go&halaman=1`),
	}
	fmt.Println(render(tRaw, data))
}

Output yang Dihasilkan:

text
=== payload yang sama, lima konteks ===
1. body: <p>&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</p>
2. attr: <p data-info="&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;">isi</p>
3. css : <p style="color: ZgotmplZ">isi</p>
4. js  : <script>var payload = "\u003cscript\u003ealert('xss')\u003c/script\u003e";</script>
5. url : <a href="%3cscript%3ealert%28%27xss%27%29%3c/script%3e">isi</a>

=== skema javascript: pada atribut URL ===
<a href="#ZgotmplZ">klik</a>
=== tipe tepercaya: HTML, JS, dan URL ===
aman: <b>markup dari editor tepercaya</b>
konfigurasi: <script>var k = {versi: 2};</script>
tautan: <a href="https://example.com/?q=go&amp;halaman=1">buka</a>

Mari kita telaah baris demi baris program di atas:

  1. Baris 20-25 (const lima): satu template berisi lima baris markup; posisi aksi {{.}} di tiap baris berbeda — teks paragraf, nilai atribut, nilai CSS, nilai JavaScript, dan nilai URL — sehingga kelima aksi mendapat escaper yang berbeda meskipun datanya sama.
  2. Baris 27-28 (Parse dan payload): parsing terjadi sekali; pada momen itu pula html/template memasangkan escaper per aksi berdasarkan konteks masing-masing.
  3. Baris 31 (render(t, payload)): eksekusi mengalirkan payload melalui escaper yang sudah terpasang; hasilnya lima output berbeda dari satu string yang sama.
  4. Baris 35-36 (template tautan): aksi di dalam href mendapat filter skema; nilai javascript:alert(1) ditolak karena bukan URL yang aman diikuti pengunjung.
  5. Baris 40-50 (template raw): tiga nilai dibungkus tipe penanda template.HTML, template.JS, dan template.URL; ketiganya dirender mentah pada konteksnya masing-masing karena dianggap sudah diverifikasi penulis program.

Membaca Hasil Escaping Per Konteks #

Baris pertama output menunjukkan konteks teks: <, >, dan ' berubah menjadi entitas &lt;, &gt;, dan &#39; sehingga browser menampilkannya sebagai teks biasa. Baris kedua tampak serupa, tetapi perannya berbeda — di dalam atribut, pengubahan tanda kutip menjadi &#34; mencegah payload " keluar" dari atribut lalu menambahkan handler onmouseover palsu. Tanpa penguncian kutip, input seperti " onmouseover="alert(1) akan membentuk atribut baru.

Baris ketiga menampilkan hasil paling mengejutkan: nilai CSS yang tidak dikenal aman diganti seluruhnya dengan ZgotmplZ — penanda khusus html/template untuk nilai yang ditolak. Penyaringan CSS menjaga kita dari muatan expression() warisan IE dan url(javascript:...) pada stylesheet. Baris keempat mengutip payload sebagai string JSON dengan \u003c untuk <; bentuk ini bukan gaya — literal </script> di dalam JavaScript akan menutup elemen script lebih awal saat HTML diparse, dan pengkodean unicode mencegahnya.

Baris kelima menerapkan percent-encoding URL: seluruh karakter berbahaya dienkode sehingga nilai tetap menjadi satu URL relatif yang tidak berbahaya. Ringkasan kelima konteks tersebut:

KonteksContoh posisiPerlakuan payload
Teks HTMLdi antara tagentitas &lt; &gt; &#39;
Nilai atributdata-info="{{.}}"entitas + penguncian tanda kutip
Nilai CSSstyle="color: {{.}}"diganti ZgotmplZ bila tak dikenal aman
Nilai JavaScript<script>var x = {{.}};</script>kutipan JSON + \u003c
Nilai URLhref="{{.}}"percent-encoding; skema berbahaya → #ZgotmplZ

Bagian kedua output memperlihatkan penyaring skema: URL javascript:alert(1) pada href dirender menjadi #ZgotmplZ — tautan mati yang jelas salahnya, jauh lebih baik daripada kode aktif. Nilai https:// dan path relatif lolos tanpa diubah.

Escape Hatch Bertipe #

Kadang kita benar-benar ingin menyuntikkan markup, konfigurasi JavaScript, atau URL lengkap dari sumber tepercaya. Untuk itu html/template menyediakan tipe pembungkus: template.HTML, template.JS, template.CSS, template.URL, dan template.HTMLAttr. Nilai yang dibungkus tipe ini dilewati dari pengaman pada konteksnya. Perhatikan detail penting pada output bagian ketiga: URL bertipe template.URL tetap mengalami & menjadi &amp; — karena posisinya tetap berada di dalam atribut HTML, pengamanan atribut tetap berlaku; yang dimatikan hanya validasi URL.

Dengan memahami konteks, kita kini siap menyusun aplikasi web utuh: template yang diurai sekali di awal, handler yang merender per permintaan, dan pengujian tanpa jaringan nyata. Itulah rangkaian chapter penutup section ini: server-side rendering.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

escaping-demo.go

Unduh berkas
go
package main

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

// render mengeksekusi template dan mengembalikan string hasilnya.
func render(t *template.Template, data any) string {
	var buf bytes.Buffer
	if err := t.Execute(&buf, data); err != nil {
		panic(err)
	}
	return buf.String()
}

func main() {
	// Payload XSS yang sama ditempatkan pada lima konteks berbeda.
	const lima = `1. body: <p>{{.}}</p>
2. attr: <p data-info="{{.}}">isi</p>
3. css : <p style="color: {{.}}">isi</p>
4. js  : <script>var payload = {{.}};</script>
5. url : <a href="{{.}}">isi</a>
`

	t := template.Must(template.New("lima").Parse(lima))
	payload := `<script>alert('xss')</script>`

	fmt.Println("=== payload yang sama, lima konteks ===")
	fmt.Println(render(t, payload))

	// Skema URL berbahaya disaring menjadi #ZgotmplZ.
	fmt.Println("=== skema javascript: pada atribut URL ===")
	tTautan := template.Must(template.New("tautan").Parse(`<a href="{{.}}">klik</a>`))
	fmt.Println(render(tTautan, "javascript:alert(1)"))

	// Tipe penanda tepercaya mematikan pengaman pada konteksnya.
	fmt.Println("=== tipe tepercaya: HTML, JS, dan URL ===")
	const raw = `aman: {{.Markup}}
konfigurasi: <script>var k = {{.Konfig}};</script>
tautan: <a href="{{.Tautan}}">buka</a>`
	tRaw := template.Must(template.New("raw").Parse(raw))
	data := map[string]any{
		"Markup": template.HTML(`<b>markup dari editor tepercaya</b>`),
		"Konfig": template.JS(`{versi: 2}`),
		"Tautan": template.URL(`https://example.com/?q=go&halaman=1`),
	}
	fmt.Println(render(tRaw, data))
}