Training lab keamanan web: HTML Injection (Client-side) — php -S + SQLite, tanpa framework.
Aplikasi HelpDesk Portal yang sengaja dibuat rentan terhadap HTML Injection (rendering markup HTML tanpa sanitasi/output encoding). Merupakan lab mandiri untuk praktik: mengamati reflected, stored, dan cross-user HTML Injection, lalu membandingkannya dengan endpoint yang sudah di-escape dan menulis laporan.
Vulnerability : CWE-79 Improper Neutralization (konteks: HTML / markup injection)
Target : HelpDesk Portal
Cakupan Web : Klien (browser rendering)
Lab ini disusun hanya untuk riset & edukasi. Jangan dipakai pada sistem/produksi milik orang lain tanpa izin. Tidak ada request keluar ke internet dari aplikasi.
- Tentang Lab
- Daftar Isi
- Teknologi
- Cara Menjalankan
- Akun & Kredensial
- Struktur Project
- Peta Halaman (Endpoint)
- Alur Data Input
- Kerentanan Utama
- Contoh Payload
- Verifikasi Vuln (Reflected)
- Verifikasi Vuln (Stored)
- Verifikasi Vuln (Cross-User)
- Panduan Laporan (Report)
- QA Otomatis
- Catatan Termux & Troubleshooting
- PHP 8.x (built-in development server
php -S) - SQLite via PDO (
pdo_sqlite) - HTML / CSS vanilla (tanpa framework, tanpa bundler)
- Session PHP (cookie
PHPSESSID)
Tidak ada dependensi composer / npm / internet.
Prasyarat: sudah terpasang php (8+), php-pdo-sqlite, dan curl.
bash jalankan.sh # seeded + jalan di http://127.0.0.1:8010
bash jalankan.sh 8080 # port kustom
bash jalankan.sh stop # matikan serverDatabase database/lab.sqlite dibuat otomatis pertama kali (seed). Untuk reset ke
kondisi awal:
bash jalankan.sh stop
rm -f database/lab.sqlite
bash jalankan.shDi Termux
chmod +xtidak berarti apa-apa — selalu jalankan denganbash.
Kredensial default hanya dicantumkan di dokumentasi ini:
| Role | Username | Password |
|---|---|---|
| admin | admin |
Admin123! |
| user | alice |
Password123! |
User lain bisa didaftarkan lewat halaman registrasi (role otomatis user).
lab-html-injection/
├── jalankan.sh # start/stop server + seed
├── index.php # landing (public)
├── register.php # daftar akun baru
├── login.php / logout.php
├── dashboard.php # beranda user + soal riset
├── profile.php # edit profil
├── user.php # lihat profil user lain (public)
├── tickets.php # daftar ticket saya
├── create-ticket.php # input description (VULN)
├── ticket.php # detail ticket (VULN - raw render)
├── ticket-secure.php # detail ticket (secure - escaped)
├── search.php # pencarian ticket (VULN - reflected)
├── security-info.php # HTML Injection vs XSS
├── report.php # template laporan bug bounty
├── remediation.php # cara memperbaiki
├── admin/index.php # dashboard admin
├── admin/tickets.php # semua ticket (cross-user meta)
├── admin/preview.php # preview ticket admin (VULN)
├── includes/
│ ├── db.php # SQLite + seed (users, profiles, tickets)
│ ├── auth.php # session, login/logout, guard role
│ └── functions.php # e(), redirect(), flash(), layout
├── assets/style.css
└── tools/ujilab.sh # QA otomatis
| Endpoint | Metode | Auth | Keterangan |
|---|---|---|---|
/index.php |
GET | publik | landing page |
/register.php |
GET/POST | publik | registrasi (role user) |
/login.php |
GET/POST | publik | login |
/logout.php |
GET | login | logout |
/dashboard.php |
GET | login | beranda + 7 pertanyaan riset |
/profile.php |
GET/POST | login | edit profil sendiri (DISAFE) |
/user.php?id= |
GET | publik | lihat profil user lain |
/tickets.php |
GET | login | daftar ticket milik sendiri |
/create-ticket.php |
GET/POST | login | buat ticket (input vuln) |
/ticket.php?id= |
GET | login | detail ticket (render vuln) |
/ticket-secure.php?id= |
GET | login | detail ticket (escape) |
/search.php?q= |
GET | login | cari ticket (reflected vuln) |
/admin/* |
GET | admin | area admin (preview vuln) |
/security-info.php |
GET | login | materi HTML Injection vs XSS |
/report.php |
GET | login | template laporan |
/remediation.php |
GET | login | panduan remediasi |
profile.php dan daftar ticket sudah di-escape — dipakai untuk membandingkan
perilaku safe path. Nomor role admin di-seed hanya melalui SQL.
POST /create-ticket.php→ menyimpansubject+description(plain, tanpa sanitasi) ke tabeltickets.GET /ticket.php?id=N→ membaca baris tersebut dan mencetaknya mentah:<?= $ticket['description'] ?>.GET /admin/preview.php?id=N→ admin membuka ticket user yang sama dengan cara render yang sama (raw) → cross-user impact.GET /search.php?q=...→ mengembalikan nilaiqlangsung ke halaman hasil (reflected).
Perbandingan: GET /ticket-secure.php?id=N menampilkan payload sebagai teks karena
htmlspecialchars(..., ENT_QUOTES|ENT_SUBSTITUTE, 'UTF-8').
- Reflected HTML Injection —
/search.php?q=<payload> - Stored HTML Injection —
descriptionticket ditampilkan mentah di/ticket.php?id=dan/admin/preview.php?id= - Cross-user — satu attacker (user biasa) membuat ticket berisi markup; saat admin membuka preview, markup tersebut ikut dirender di browser admin.
Catatan penting (lihat security-info.php):
HTML Injection membuktikan render markup tanpa encoding (vulnerability). Belum tentu otomatis XSS — untuk klaim XSS perlu membuktikan eksekusi JavaScript di konteks korban. Eksekusi JS bukan bagian dari lab ini.
Gunakan kode di bawah pada q (reflected) atau description (stored). Semua
hanya markup, tanpa JavaScript.
<h2>Injected Heading</h2>
<b>Bold injected text</b>
<br>line break injected
<details><summary>Spoiler injected</summary>Hello researcher</details>
<marquee>Injected marquee</marquee>
<table border=1><tr><td>Injected table</td></tr></table>
<ul><li>Item injected 1</li><li>Item injected 2</li></ul>
<img src="http://nonexistent.invalid/x.png"> <!-- broken icon: proof of render -->
<iframe src="/"> <!-- embeds halaman -->
<style>body{background:#fdd}</style>
<div style="position:fixed;inset:0;background:#fff;z-index:9999">overlay</div>Cara cepat lewat cURL (ingat: URL harus di-encode):
BASE=http://127.0.0.1:8010
J=/data/data/com.termux/files/usr/tmp/opencode/ck.curl
curl -s -c $J -b $J --data-urlencode 'username=alice' --data-urlencode 'password=Password123!' $BASE/login.php >/dev/null
# Stored:
curl -s -c $J -b $J --data-urlencode 'subject=Test' --data-urlencode 'description=<h2>Injected</h2>' $BASE/create-ticket.php -D - -o /dev/null | grep -i location
# Reflected:
curl -s -c $J -b $J "$BASE/search.php?q=%3Ch2%3EInjected%3C/h2%3E" | grep -o '<h2>Injected</h2>'- Login sebagai
alice. - Buka:
http://127.0.0.1:8010/search.php?q=%3Ch2%3EInjected%3C%2Fh2%3E - Amati:
<h2>Injected</h2>dirender sebagai heading di blok hasil pencarian. Nilaiqtidak di-escape sama sekali.
- Login sebagai
alice. create-ticket.php→ subject bebas, description:<h2>Injected from ticket</h2>- Buka
/ticket.php?id=<id ticket>→ description dirender sebagai heading. - Bandingkan
/ticket-secure.php?id=<id>→ tampil sebagai teks (<h2>...).
- Sebagai
alice, buat ticket dengan description berisi markup (langkah 12). - Logout, login sebagai admin (
admin / Admin123!). - Buka
/admin/tickets.php→ lihat subject ticket milik alice. - Buka
/admin/preview.php?id=<id>→ markup dari akun alice ikut dirender di browser sesi admin. - Impact: attacker user biasa dapat memodifikasi penampilan halaman yang dikonsumsi user lain / admin (defacing ringan, konten menyesatkan, potensi naik kelas ke XSS tergantung konteks).
Template siap salin ada di /report.php (endpoint dalam aplikasi). Struktur:
TITLE: Stored HTML Injection in Support Ticket Description Allows Arbitrary HTML Rendering
SEVERITY: Low (default) — naik bergantung dampak
AFFECTED ENDPOINT: /create-ticket.php (input) ? /ticket.php?id= & /admin/preview.php?id= (render)
PARAMETER: description
DESCRIPTION / STEPS TO REPRODUCE / IMPACT / REMEDIATION
Rekomendasi bagi researcher: selalu sertakan Steps to Reproduce yang bisa dijalankan ulang, bukti render (screenshot atau curl), dan perbandingan dengan endpoint yang sudah di-escape.
Server sedang berjalan, cek seluruh flow + reproduksi vuln sekaligus:
bash tools/ujilab.shQA: register → login → logout → profil → create ticket → render stored
(ticket vs ticket-secure) → reflected search → cross-user admin → role check (403
untuk user biasa) → support pages → bebas warning/fatal. Server di-log ke
php-server.log.
/tmptak bisa ditulis di Termux — cookie jar QA memakai/data/data/com.termux/files/usr/tmp/opencode. SetTditools/ujilab.shbila pindah mesin.chmod +xtidak berfungsi di Termux — selalubash jalankan.sh.- Port default
8010(hindari 8000 yang sering dipakai server lain). Jika bentrok:bash jalankan.sh 8080, lalu ubahBditools/ujilab.sh. - Reset penuh: stop server, hapus
database/lab.sqlite, mulai lagi. - Resolver Android mengabaikan
/etc/hosts; pakai--resolvebila butuh test domain fiktif (tidak relevan untuk lab ini). - Ingat: komputer admin mengirim
<form>ber-action— deskripsi yang Anda lihat di/admin/preview.phpadalah render dari data user tanpa filter.
Lisensi: Untuk keperluan riset & edukasi. Jangan digunakan untuk menyerang sistem yang bukan milik Anda. Namun ISI materi, kode, atau alur pengujian di lab ini boleh disalin & dikembangkan dengan menyebutkan sumber.
