Memahami Risiko Path Traversal di Datasette App
Menyajikan antarmuka kustom di atas Datasette menggunakan plugin atau ASGI route kustom mempermudah integrasi frontend dan backend data. Masalah keamanan muncul ketika handler static file kustom mengambil path berkas langsung dari parameter rute URL tanpa pembatasan direktori kerja (directory jail). Tanpa isolasi ketat, penyerang dapat mengeksploitasi sekuens relatif (../) untuk membaca berkas basis data SQLite atau berkas konfigurasi sistem host.
Studi Kasus: Kebocoran SQLite dan Konfigurasi Host
Pada skenario umum implementasi custom app Datasette, developer kerap membuat ASGI handler sederhana untuk menyajikan aset CSS, JavaScript, atau berkas HTML statis. Pertimbangkan route rentan berikut:
import os
from pathlib import Path
from datasette.utils.asgi import Response
STATIC_DIR = Path("/app/static").resolve()
async def serve_static(request):
filename = request.url_vars.get("path", "")
# Vulnerable: konkatenasi langsung tanpa memeriksa batas STATIC_DIR
target_file = STATIC_DIR / filename
if target_file.is_file():
return Response.text(target_file.read_text(), content_type="text/plain")
return Response.text("Not Found", status=404)
Identifikasi Kerentanan via curl
Ketika aplikasi dijalankan, permintaan HTTP normal berfungsi semestinya. Namun, jika URL path dikirim dengan path traversal ter-encode atau path relatif langsung, berkas internal dapat terekspos:
curl -s "http://localhost:8001/-/static/../data.db" --output leak.db
curl -s "http://localhost:8001/-/static/../../../../etc/passwd"
Karakter ../ menyelesaikan path relatif di luar /app/static, menyebabkan handler membaca berkas biner SQLite (data.db) atau berkas konfigurasi host yang memiliki izin baca pada proses ASGI server.
Root Cause: Kegagalan Resolusi Path Relatif
Fungsi Path.__truediv__ (operator /) pada modul pathlib tidak otomatis membatasi traversal jika argumen yang digabungkan memuat ... Selama path hasil kalkulasi berujung pada berkas yang valid di filesystem, pemanggilan is_file() tetap bernilai True.
Solusi 1: Validasi Safe Path Menggunakan os.path.commonpath
Pendekatan defensif langsung pada kode kustom adalah menerapkan resolusi absolut (canonicalization) via Path.resolve() dan memvalidasi bahwa basis direktori tetap menjadi induk menggunakan os.path.commonpath.
import os
from pathlib import Path
from datasette.utils.asgi import Response
BASE_DIR = Path("/app/static").resolve()
def resolve_safe_path(base_dir: Path, relative_path: str) -> Path | None:
# Normalisasi path untuk membuang traversal relatif
resolved = (base_dir / relative_path).resolve()
# Verifikasi apakah resolved path berada di dalam base_dir
try:
if os.path.commonpath([str(base_dir), str(resolved)]) == str(base_dir):
return resolved
except ValueError:
# Menangani perbedaan drive letter pada platform Windows
return None
return None
async def serve_static_secure(request):
filename = request.url_vars.get("path", "")
target_file = resolve_safe_path(BASE_DIR, filename)
if not target_file or not target_file.is_file():
return Response.text("Forbidden / Not Found", status=404)
return Response.text(target_file.read_text(), content_type="text/plain")
Alternatif modern pada Python 3.9+ adalah resolved.is_relative_to(base_dir), yang melemparkan status valid hanya jika resolved adalah anak dari base_dir.
Solusi 2: Gunakan Fitur Static Mount Datasette
Pendekatan paling aman adalah menghindari penulisan custom file server manual. Datasette menyediakan mekanisme bawaan untuk menyajikan static file yang sudah dilengkapi proteksi jail direktori internal.
Jalankan Datasette dengan flag --static atau definisikan pada berkas metadata.json / konfigurasi aplikasi:
datasette serve data.db --static static:/app/static
Konfigurasi di atas memetakan prefix URL /-/static/ ke direktori lokal /app/static secara aman. Mesin perutean Datasette secara otomatis menolak request yang mencoba keluar dari batasan direktori tersebut, mengeliminasi risiko kebocoran database atau file host.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!