Template adalah berkas yang berisi kerangka HTML beserta titik-titik sisipan untuk nilai dinamis. Disiplin templating berarti tiga hal itu dikelola dengan aturan yang tetap, yaitu escaping pada setiap keluaran, satu layout untuk struktur halaman, dan partial untuk bagian yang berulang. Topik ini membangun ketiganya sebagai fungsi PHP biasa tanpa library tambahan, lalu membuktikan kerjanya dengan masukan yang memuat tag skrip. Setelah membacanya, setiap halaman aplikasi dapat disusun dari bagian yang sama dan setiap nilai dinamis lolos melalui satu pintu yang mudah diaudit.
Satu fungsi escaping untuk semua keluaran#
Escaping, yakni pelolosan karakter khusus HTML, mengubah karakter seperti < menjadi bentuk yang aman ditampilkan, sehingga nilai dinamis tidak pernah dapat mengubah struktur halaman. Topik pencegahan XSS membahas ancamannya secara rinci, sedangkan topik ini menata tempat pemanggilannya. Aturan yang dipakai adalah satu fungsi pembungkus untuk seluruh aplikasi, sehingga pilihan parameter escaping ditetapkan sekali dan diperiksa di satu tempat.
<?php
declare(strict_types=1);
// satu pintu escaping untuk seluruh template
function e(?string $nilai): string
{
return htmlspecialchars($nilai ?? '', ENT_QUOTES, 'UTF-8');
}
// layout: membungkus isi halaman dengan bagian atas dan bawah
function render(string $judul, string $isi): void
{
require_once 'bagian-atas.php';
require_once 'bagian-bawah.php';
bagian_atas(e($judul));
echo '<main>' . PHP_EOL;
echo $isi;
echo '</main>' . PHP_EOL;
bagian_bawah();
}Fungsi e membungkus htmlspecialchars dengan dua keputusan yang tetap, yaitu ENT_QUOTES agar tanda kutip juga diloloskan dan UTF-8 agar pengodean tidak bergantung pada konfigurasi server. Parameter yang boleh kosong diberi tipe ?string dan nilai null diubah menjadi string kosong, sehingga panggilan dari template tidak pernah memicu error nilai null. Fungsi render menjadi layout, yaitu kerangka yang sama untuk setiap halaman, dengan isi halaman diserahkan melalui parameter $isi.
Perhatikan bahwa judul halaman di-escape di dalam render sebelum diserahkan ke partial bagian atas. Penempatan seperti itulah yang dimaksud pintu terpusat, yaitu nilai mentah masuk lewat e pada titik terakhir sebelum dicetak, bukan diserahkan mentah lalu diharapkan di-escape oleh pihak lain.
Partial untuk bagian yang berulang#
Partial adalah potongan template yang dipakai berulang antarhalaman, misalnya bagian pembuka berisi <head> dan navigasi serta bagian penutup berisi penutup <body>. Kedua bagian itu hampir tidak pernah berubah, sedangkan isinya disalin di setiap halaman bila ditulis manual. Menjadikannya partial menghapus salinan tersebut, karena perubahan navigasi cukup dilakukan di satu berkas.
<?php
declare(strict_types=1);
// partial bagian atas: struktur pembuka yang sama untuk setiap halaman
function bagian_atas(string $judulHalaman): void
{
echo '<!DOCTYPE html>' . PHP_EOL;
echo '<html lang="id">' . PHP_EOL;
echo '<head>' . PHP_EOL;
echo '<meta charset="utf-8">' . PHP_EOL;
echo '<title>' . $judulHalaman . '</title>' . PHP_EOL;
echo '</head>' . PHP_EOL;
echo '<body>' . PHP_EOL;
echo '<nav><a href="/">Beranda</a></nav>' . PHP_EOL;
}<?php
declare(strict_types=1);
// partial bagian bawah: struktur penutup yang sama untuk setiap halaman
function bagian_bawah(): void
{
echo '</body>' . PHP_EOL;
echo '</html>' . PHP_EOL;
}Kedua partial ditulis sebagai fungsi, bukan berkas yang langsung mencetak, sehingga masing-masing tetap dapat dimuat tanpa menimbulkan keluaran yang tidak diinginkan. Parameter judulHalaman pada bagian_atas berisi judul yang sudah di-escape oleh render, dan kesepakatan itu dituliskan pada bagian sebelumnya agar tidak ada pelolosan ganda.
Membuktikan escaping dengan masukan berbahaya#
Komentar pengunjung adalah contoh masukan yang isinya tidak dapat diduga, termasuk tag <script>. Skrip berikut menyusun isi halaman dari komentar semacam itu melalui pintu e, lalu merendernya dengan layout dan partial yang sudah dibuat.
<?php
declare(strict_types=1);
require 'Template.php';
// komentar pengunjung yang memuat tag skrip
$komentar = '<script>alert(1)</script>';
$isi = '<p>' . e($komentar) . '</p>' . PHP_EOL;
render('Komentar Pengunjung', $isi);<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Komentar Pengunjung</title>
</head>
<body>
<nav><a href="/">Beranda</a></nav>
<main>
<p><script>alert(1)</script></p>
</main>
</body>
</html>Tag <script> pada masukan berubah menjadi <script> di keluaran, sehingga browser menampilkannya sebagai teks biasa dan tidak pernah mengeksekusinya. Hasil yang sama muncul ketika halaman diminta melalui HTTP, sebagaimana diuji perintah berikut.
# jalankan server bawaan pada port 8110 dari folder aplikasi
php -S localhost:8110 >/dev/null 2>&1 &
sleep 1
# minta halaman bukti escaping
curl -s http://localhost:8110/bukti-escaping.php
# hentikan server
kill %1<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Komentar Pengunjung</title>
</head>
<body>
<nav><a href="/">Beranda</a></nav>
<main>
<p><script>alert(1)</script></p>
</main>
</body>
</html>Karena keluaran yang aman berasal dari satu panggilan e, audit keamanan template cukup memeriksa dua hal, yaitu setiap nilai dinamis melewati e dan tidak ada pemanggilan htmlspecialchars yang parameternya berbeda di tempat lain. Struktur layout dan partial menambah manfaat yang sama untuk sisi tampilan, yaitu satu tempat perubahan untuk seluruh halaman.
Kesalahan umum#
Tiga kesalahan berikut paling sering muncul saat menyusun template sendiri.
- Satu nilai dinamis dicetak langsung tanpa
edengan alasan isinya sudah dianggap aman. Cukup satu nilai yang terlewat untuk membuka celah XSS, sehingga pemeriksaan dilakukan pada setiap titik cetak, bukan pada dugaan sumber data. - Pelolosan dilakukan saat data masuk, bukan saat dicetak. Nilai yang di-escape lebih awal lalu disimpan menjadi rusak saat dipakai untuk konteks lain seperti JSON atau surat elektronik. Perbaikannya adalah menyimpan nilai mentah dan meng-escape pada titik keluaran.
- Kepala dan kaki HTML disalin manual ke setiap halaman. Perubahan navigasi harus diulang di banyak berkas dan pasti ada yang terlewat. Perbaikannya adalah memindahkan bagian yang sama menjadi partial yang dipanggil layout.
Ringkasan#
- Escaping dilakukan pada titik keluaran melalui satu fungsi pembungkus
eyang menetapkanENT_QUOTESdanUTF-8sekali untuk seluruh aplikasi. - Layout berupa fungsi
renderyang membungkus isi halaman dengan partial bagian atas dan bawah, sehingga struktur yang sama ditulis satu kali. - Partial berupa fungsi yang dimuat dengan
require_once, dan parameternya berisi nilai yang sudah di-escape oleh layout. - Masukan berisi
<script>yang lolos melaluietampil sebagai teks<script>, baik saat dijalankan lewat CLI maupun melalui HTTP.
Referensi#
- htmlspecialchars — PHP Manual
— Dokumentasi pelolosan karakter khusus HTML beserta flag
ENT_QUOTES. - require_once — PHP Manual — Dokumentasi pemuatan berkas yang mencegah deklarasi ganda.
- Templating — PHP The Right Way — Bagian templating yang menyinggung penggunaan template mesin dan pelolosan otomatis.