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 < — 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:
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:
=== payload yang sama, lima konteks ===
1. body: <p><script>alert('xss')</script></p>
2. attr: <p data-info="<script>alert('xss')</script>">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&halaman=1">buka</a>Mari kita telaah baris demi baris program di atas:
- 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. - Baris 27-28 (
Parsedanpayload): parsing terjadi sekali; pada momen itu pula html/template memasangkan escaper per aksi berdasarkan konteks masing-masing. - Baris 31 (
render(t, payload)): eksekusi mengalirkan payload melalui escaper yang sudah terpasang; hasilnya lima output berbeda dari satu string yang sama. - Baris 35-36 (template
tautan): aksi di dalamhrefmendapat filter skema; nilaijavascript:alert(1)ditolak karena bukan URL yang aman diikuti pengunjung. - Baris 40-50 (template
raw): tiga nilai dibungkus tipe penandatemplate.HTML,template.JS, dantemplate.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 <, >, dan ' sehingga browser menampilkannya sebagai teks biasa. Baris kedua tampak serupa, tetapi perannya berbeda — di dalam atribut, pengubahan tanda kutip menjadi " 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:
| Konteks | Contoh posisi | Perlakuan payload |
|---|---|---|
| Teks HTML | di antara tag | entitas < > ' |
| Nilai atribut | data-info="{{.}}" | entitas + penguncian tanda kutip |
| Nilai CSS | style="color: {{.}}" | diganti ZgotmplZ bila tak dikenal aman |
| Nilai JavaScript | <script>var x = {{.}};</script> | kutipan JSON + \u003c |
| Nilai URL | href="{{.}}" | 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 & — 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 #
- Dokumentasi package html/template
— penjelasan contextual autoescaping dan tipe
template.HTML. - Go Security — ringkasan praktik keamanan aplikasi Go dari dokumentasi resmi.
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
escaping-demo.go
Unduh berkaspackage 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))
}