Pemindai otomatis (scanner) menjalankan ratusan pemeriksaan dalam waktu yang tidak mungkin dicapai pengujian manual, tetapi keluarannya tidak pernah sekadar daftar kerentanan yang siap dipercaya. Topik ini membahas cara kerja pemindai, memperkenalkan OWASP ZAP sebagai alat open source yang umum digunakan, lalu mempraktikkan pemindaian kecil terhadap server lab lokal beserta verifikasi manualnya. Setelah topik ini, temuan pemindai dapat dipilah menjadi temuan valid dan false positive dengan alasan yang dapat dipertanggungjawabkan.
Cara kerja pemindai otomatis#
Pemindai bekerja dalam dua tahap yang saling mengisi. Tahap pemetaan menggunakan crawler, yaitu komponen yang mengikuti tautan dan formulir untuk membangun daftar halaman yang dapat dijangkau. Tahap pemeriksaan kemudian menjalankan aturan pada setiap halaman itu, mulai dari pemeriksaan pasif yang hanya membaca respons, misalnya kelengkapan header keamanan, sampai pemeriksaan aktif yang mengirim payload uji lalu menilai responsnya.
OWASP ZAP (Zed Attack Proxy) adalah pemindai open source dari OWASP yang memadukan proxy pencegat dengan pemindai pasif dan aktif. Pemindaiannya berjenjang, mulai dari baseline scan yang cepat dan tanpa pemeriksaan aktif sampai full scan yang mengirim payload uji ke titik masuk yang ditemukan. Perintah berikut menjalankan pemindaian dasar ZAP dari kontainer Docker, satu cara pelaksanaan yang digunakan pada dokumentasi resminya.
# tidak-dijalankan
# blok ini membutuhkan Docker serta target yang diizinkan, keduanya
# tidak tersedia pada sandbox topik ini
docker run --rm -v "$PWD":/zap/wrk/:rw ghcr.io/zaproxy/zaproxy:stable \
zap-baseline.py -t http://host.docker.internal:8921 -r laporan-zap.htmlPemindaian dasar menghasilkan laporan berperingkat, mulai dari peringatan informasi sampai peringatan berisiko tinggi, lengkap dengan rujukan penjelasannya. Peringkat itu berasal dari aturan umum, sehingga tingkat risikonya belum mempertimbangkan konteks aplikasi. Penilaian konteks inilah pekerjaan yang tersisa bagi penguji dan tidak dapat dialihkan ke alat.
Lab: pemindai sederhana dan lapisan proxy#
Skrip berikut meniru dua lapisan yang biasa dipindai, yaitu aplikasi yang berjalan langsung dan jalur produksi yang melewati lapisan reverse proxy. Pemindainya memeriksa tiga header keamanan pada respons halaman utama dan menguji pantulan input pada endpoint pencarian, kemudian bagian verifikasi mengulang pemeriksaan yang sama melalui jalur proxy. Jalankan berkas dengan perintah python3 pindai-pasif.py.
"""Pemindai sederhana untuk server lab lokal beserta verifikasi manualnya.
Tahap pemindaian memeriksa header keamanan pada respons aplikasi dan
menguji pantulan input pada endpoint pencarian. Tahap verifikasi
mengulang pemeriksaan yang sama melalui jalur produksi yang melewati
lapisan proxy, sehingga terlihat temuan mana yang menjadi false positive
dan mana yang tetap valid. Kedua server berjalan sebagai thread di
dalam skrip yang sama, sehingga keluarannya deterministik."""
import threading
import urllib.parse
import urllib.request
from http.server import BaseHTTPRequestHandler, HTTPServer
PORT_APLIKASI = 8921
PORT_PROXY = 8922
HEADER_KEAMANAN = ("Content-Security-Policy", "X-Content-Type-Options", "X-Frame-Options")
HEADER_PROXY = {
"Content-Security-Policy": "default-src 'self'",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
}
class PenanganAplikasi(BaseHTTPRequestHandler):
def kirim(self, isi):
data = isi.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
bagian = urllib.parse.urlparse(self.path)
query = urllib.parse.parse_qs(bagian.query)
if bagian.path == "/":
# halaman lab tanpa satu pun header keamanan
self.kirim("<h1>Papan pengumuman lab</h1><p>Selamat datang.</p>")
elif bagian.path == "/cari":
# endpoint lab yang memantulkan input tanpa pengodean
kata = query.get("q", [""])[0]
self.kirim(f"<h1>Hasil untuk: {kata}</h1>")
else:
self.send_response(404)
self.send_header("Content-Length", "0")
self.end_headers()
def log_message(self, format, *args):
pass
class PenanganProxy(BaseHTTPRequestHandler):
"""Peran reverse proxy: meneruskan permintaan ke aplikasi, lalu
menambahkan header keamanan pada respons sebelum sampai ke klien."""
def do_GET(self):
url = f"http://127.0.0.1:{PORT_APLIKASI}{self.path}"
with urllib.request.urlopen(url, timeout=5) as jawaban:
isi = jawaban.read()
tipe = jawaban.headers.get("Content-Type", "text/html")
self.send_response(200)
self.send_header("Content-Type", tipe)
self.send_header("Content-Length", str(len(isi)))
for nama, nilai in HEADER_PROXY.items():
self.send_header(nama, nilai)
self.end_headers()
self.wfile.write(isi)
def log_message(self, format, *args):
pass
def ambil(port, target):
url = f"http://127.0.0.1:{port}{target}"
with urllib.request.urlopen(url, timeout=5) as jawaban:
return jawaban.status, dict(jawaban.headers), jawaban.read().decode("utf-8")
def pindai(port, label):
print(f"== Pemindaian terhadap {label} ==")
temuan = []
status, header, isi = ambil(port, "/")
for nama in HEADER_KEAMANAN:
if nama not in header:
temuan.append(("header", nama, "/"))
print(f"[TEMUAN] header {nama} tidak ada pada GET /")
muatan = "uji<b>123"
status, header, isi = ambil(port, "/cari?q=" + urllib.parse.quote(muatan))
if muatan in isi:
temuan.append(("pantulan", muatan, "/cari"))
print(f"[TEMUAN] input terpantulkan mentah pada /cari (parameter q): {muatan}")
return temuan
def verifikasi(temuan, port_aplikasi, port_proxy):
print("== Verifikasi manual melalui jalur produksi (proxy) ==")
for jenis, isi_t, lokasi in temuan:
if jenis == "header":
status, header, isi = ambil(port_proxy, lokasi)
if isi_t in header:
print(
f"[FALSE POSITIVE] {isi_t}: ternyata diterapkan pada lapisan proxy "
f"({header[isi_t]})"
)
else:
print(f"[VALID] header {isi_t} memang tidak sampai ke klien")
else:
target = "/cari?q=" + urllib.parse.quote(isi_t)
status, header, isi = ambil(port_proxy, target)
if isi_t in isi:
print(
f"[VALID] pantulan mentah {isi_t} tetap ada melalui proxy, "
"kerentanan perlu diperbaiki"
)
else:
print(f"[FALSE POSITIVE] pantulan {isi_t} tidak sampai ke klien")
def main():
aplikasi = HTTPServer(("127.0.0.1", PORT_APLIKASI), PenanganAplikasi)
proxy = HTTPServer(("127.0.0.1", PORT_PROXY), PenanganProxy)
for server in (aplikasi, proxy):
threading.Thread(target=server.serve_forever, daemon=True).start()
try:
temuan = pindai(PORT_APLIKASI, f"aplikasi 127.0.0.1:{PORT_APLIKASI}")
print()
verifikasi(temuan, PORT_APLIKASI, PORT_PROXY)
finally:
aplikasi.shutdown()
aplikasi.server_close()
proxy.shutdown()
proxy.server_close()
if __name__ == "__main__":
main()== Pemindaian terhadap aplikasi 127.0.0.1:8921 ==
[TEMUAN] header Content-Security-Policy tidak ada pada GET /
[TEMUAN] header X-Content-Type-Options tidak ada pada GET /
[TEMUAN] header X-Frame-Options tidak ada pada GET /
[TEMUAN] input terpantulkan mentah pada /cari (parameter q): uji<b>123
== Verifikasi manual melalui jalur produksi (proxy) ==
[FALSE POSITIVE] Content-Security-Policy: ternyata diterapkan pada lapisan proxy (default-src 'self')
[FALSE POSITIVE] X-Content-Type-Options: ternyata diterapkan pada lapisan proxy (nosniff)
[FALSE POSITIVE] X-Frame-Options: ternyata diterapkan pada lapisan proxy (DENY)
[VALID] pantulan mentah uji<b>123 tetap ada melalui proxy, kerentanan perlu diperbaikiKeempat temuan pada tahap pemindaian benar sebagaimana dilihat pemindai, yaitu aplikasi memang mengirim respons tanpa ketiga header dan memang memantulkan input mentah. Verifikasi melalui jalur produksi mengubah keputusannya, karena ketiga header ternyata diterapkan pada lapisan proxy sebelum respons sampai ke klien, sedangkan pantulan mentah tetap ada. Satu temuan bertahan dan tiga temuan mundur sebagai false positive, dan kedua kelompok itu memerlukan catatan yang sama rincinya.
Pembandingan dua lapisan ini meniru keadaan yang sering ditemui pada proyek nyata, yaitu laporan pemindai diproduksi dari alamat aplikasi, sedangkan pengguna menjangkaunya melalui domain proxy perusahaan. Ketika kedua lapisan tidak dipisahkan, temuan lapisan aplikasi dilaporkan apa adanya dan tim pengembang menolaknya karena tidak muncul pada jalur pengguna. Pemisahan lapisan pada lab membuat akar perbedaannya terlihat dalam satu keluaran.
Memilah false positive#
False positive adalah temuan yang dilaporkan pemindai tetapi tidak benar dalam konteks penyebaran aplikasi. Pemindai menghasilkannya karena bekerja dengan aturan umum tanpa mengenal konteks, misalnya memindai lapisan aplikasi padahal header keamanan diterapkan pada lapisan proxy, melihat pola respons yang mirip kerentanan, atau menandai endpoint yang memang sengaja mengembalikan data tertentu.
Pemilahan dilakukan dengan mengulang langkah pemindai secara manual untuk setiap temuan, memburu bukti yang mendukung atau membantahnya, lalu menyatakan keputusannya beserta alasan. Temuan yang bertahan menjadi bahan laporan, sedangkan false positive tetap tercatat bersama alasannya agar temuan yang sama tidak muncul kembali sebagai kejutan pada pemindaian berikutnya.
Alur lengkap dari pemindaian sampai keputusan digambarkan sebagai berikut.
Rasio temuan valid terhadap false positive bervariasi antaraplikasi dan antaralat, sehingga tidak ada angka baku yang dapat dikutip. Yang konstan adalah pekerjaan verifikasinya, karena setiap temuan yang disampaikan kepada pemilik aplikasi wajib sudah melewati pemeriksaan manual. Laporan yang berisi temuan mentah tanpa pemilahan cepat kehilangan kepercayaan penerimanya.
Kesalahan umum#
- Mempercayai peringkat risiko bawaan pemindai tanpa meninjau konteks. Tingkat risiko final ditetapkan setelah verifikasi, bukan disalin dari laporan alat.
- Menjalankan pemindai aktif pada lingkungan produksi tanpa izin dan tanpa batas laju. Aturan mainnya sama dengan pengujian manual, yaitu hanya target yang diizinkan.
- Mengirim laporan yang penuh temuan mentah. Pemilahan false positive dilakukan sebelum laporan dikirim, karena kredibilitas hilang pada temuan pertama yang terbantahkan.
- Menganggap satu pemindaian bersih berarti aplikasi aman. Pemindai menguji pola yang dikenalnya, sedangkan cacat logika dan kontrol akses yang khusus tetap memerlukan pengujian manual.
Pola tersebut berpangkal pada satu kekeliruan, yaitu memperlakukan keluaran pemindai sebagai hasil akhir. Keluaran pemindai adalah daftar hipotesis yang belum selesai, dan pekerjaan yang menuntaskannya adalah verifikasi yang berpijak pada bukti. Pembagian kerjanya jelas, yaitu alat memperluas jangkauan pemeriksaan, sedangkan penguji menetapkan kebenaran dan tingkat risikonya.
Ringkasan#
- Pemindai memetakan aplikasi dengan crawler, lalu menjalankan pemeriksaan pasif dan aktif pada halaman yang ditemukannya.
- OWASP ZAP menyediakan pemindaian berjenjang dari baseline yang pasif sampai full scan yang aktif, dan seluruhnya menuntut target yang diizinkan.
- Temuan pemindai wajib diverifikasi manual karena keluarannya berisi false positive, misalnya temuan pada lapisan yang tidak mewakili jalur produksi.
- Temuan valid dan false positive sama-sama dicatat beserta alasannya.
Referensi#
- OWASP ZAP — OWASP Foundation — Dokumentasi resmi ZAP, termasuk pemindaian baseline dan full scan dari baris perintah.
- OWASP Web Security Testing Guide — OWASP Foundation — Kerangka pengujian yang menjadi dasar aturan pemeriksaan pada pemindai.
- Web Security Academy — PortSwigger — Materi pendukung untuk memahami kerentanan yang dicari oleh aturan pemindai.