Files
2026-10-20 13:16:38 +09:00

8.1 KiB
Raw Permalink Blame History

Keamanan — Kalkulator Cashback SERUBI

Ditulis untuk: admin server yang men-deploy aplikasi ke internet.

Sudah ditangani di aplikasi (berlaku otomatis setelah file diunggah)

Area Perlindungan
Header keamanan CSP ketat dengan nonce (tanpa unsafe-inline), X-Frame-Options: DENY, frame-ancestors 'none', nosniff, Referrer-Policy, Permissions-Policy, COOP/CORP. HSTS aktif otomatis saat diakses lewat HTTPS.
XSS Semua output di-escape (e()); input bertipe array dibuang; nama admin menolak < > dan karakter kontrol.
SQL injection Semua query memakai PDO prepared statement, dengan emulasi prepare dimatikan.
CSRF Token per sesi di setiap form POST, dibandingkan dengan hash_equals; cookie SameSite=Strict. Token diganti saat login.
Login CAPTCHA self-hosted (gambar dari server, sekali pakai, 5 menit) diperiksa sebelum password. Kunci 15 menit setelah 5 kali gagal per IP. Tidak ada kunci per username (mencegah akun admin dikunci orang lain); kegagalan beruntun dicatat di Riwayat dan diberitahukan ke pemilik akun saat login berikutnya. Pesan error sama untuk username atau password salah; hash palsu supaya waktu respons tidak membocorkan username.
Dua faktor (2FA) Wajib untuk semua admin: TOTP (RFC 6238) dari Google/Microsoft Authenticator. Diaktifkan saat login pertama (QR code dibuat lokal, tanpa layanan luar). Rahasia OTP dienkripsi AES-256-GCM dengan app_key; kode tidak bisa dipakai ulang; 5 kode salah membatalkan login; 10 kode pemulihan sekali pakai. Sesi admin baru dibuat setelah OTP benar.
Hak akses admin Peran super admin dan admin. Hanya super admin yang dapat menambah, menonaktifkan, me-reset password/OTP, dan mengubah peran admin lain, dan setiap aksi itu meminta password super admin (re-autentikasi). Password hasil reset wajib diganti saat login berikutnya. Super admin aktif terakhir tidak dapat dinonaktifkan atau diturunkan.
Parameter bank Halaman publik menghitung di browser (instan, tetap jalan offline) dengan model koefisien: biaya internal dilebur menjadi satu angka margin per tenor (engine_client_model(), assets/js/engine-public.js). GWM, kas, porsi PUAB/kredit, rate PUAB, bunga kredit, OHC, dan rate deposito tidak dikirim. Cetak struk dan Salin ringkasan mengambil angka resmi dari server (calc.php); tanpa koneksi ditandai "belum terverifikasi server". Rincian perhitungan hanya untuk admin yang login.
Kredensial Tidak ada password di config.php. Kredensial database dan app_key dibaca dari environment variable atau berkas .serubi/secrets.php: di luar document root bila memungkinkan, atau di folder .serubi/ dalam folder aplikasi yang ditolak dari akses HTTP (403). Contoh: database/secrets.example.php.
Beban database Tidak ada migrasi otomatis per request; parameter di-cache (5 menit, dihapus otomatis setiap admin menyimpan).
Password 10–72 karakter, berisi huruf, angka, dan simbol, dan tidak boleh memuat username. Disimpan dengan bcrypt (password_hash).
Sesi ID baru setiap kali login dan setiap 15 menit; habis setelah 30 menit tidak aktif atau 8 jam sejak login; HttpOnly, Secure (di HTTPS), strict mode. Satu sesi aktif per akun: login di browser/perangkat lain otomatis mengakhiri sesi sebelumnya (dengan pesan). Mengganti password juga mengakhiri sesi lain akun tersebut.
Admin pertama Form "buat admin pertama" mati secara default. Admin pertama dibuat lewat konsol (--create-admin), atau form diaktifkan sementara dengan setup_key minimal 16 karakter.
Bocoran informasi Error PHP tidak pernah ditampilkan (hanya dicatat di log); halaman error polos; X-Powered-By dihapus.
Method HTTP Hanya GET/HEAD/POST yang diterima; PUT/DELETE/OPTIONS/PATCH/TRACK ditolak.
File internal config.php, includes/, database/, tests/, docs/, file .md/.sql/.json/.bak/~, dan semua dotfile (.git, .env, …) diblokir. Listing folder dimatikan.
Input berlebih Maksimal 30 baris per tabel; nominal maksimal Rp1.000 triliun.
Cache Halaman admin dikirim dengan no-store.

WAJIB dikerjakan di server

Bagian ini tidak bisa diatur dari aplikasi. Biasanya inilah sumber temuan pentest yang tersisa.

  1. HTTPS. Pasang sertifikat TLS, lalu aktifkan pengalihan HTTP→HTTPS lewat environment SERUBI_FORCE_HTTPS=1 atau 'force_https' => true di config.php. Izinkan hanya TLS 1.2/1.3:
    SSLProtocol -all +TLSv1.2 +TLSv1.3
    SSLHonorCipherOrder off
    
    Kalau aplikasi berada di belakang reverse proxy atau load balancer, isi IP proxy tersebut di SERUBI_TRUSTED_PROXIES.
  2. Sembunyikan versi dan matikan TRACE di httpd.conf. Pengaturan ini tidak bisa lewat .htaccess:
    ServerTokens Prod
    ServerSignature Off
    TraceEnable Off
    
  3. php.ini produksi:
    expose_php = Off
    display_errors = Off
    log_errors = On
    session.cookie_secure = 1
    session.use_strict_mode = 1
    
  4. Perbarui software. PHP 8.1 sudah end-of-life sejak 31 Desember 2025, dan OpenSSL 1.1.1 sejak September 2023. Pentest akan melaporkan versi yang usang. Gunakan PHP 8.3 atau 8.4, Apache 2.4 versi terbaru, dan OpenSSL 3.x. Aplikasi ini kompatibel dengan PHP 7.4 sampai 8.4.
  5. Jangan pakai user root untuk MySQL. Jalankan sekali dengan akun root:
    php database/migrate.php --create-db-user
    
    Perintah ini membuat user serubi_app yang hanya punya hak SELECT/INSERT/UPDATE/DELETE pada database calcserubi. Salin user dan password yang dicetak ke config.php atau ke environment SERUBI_DB_USER/SERUBI_DB_PASS. Setelah itu ganti juga password root MySQL, karena password yang sekarang pernah tertulis di config.
  6. MySQL jangan terbuka ke internet. Set bind-address = 127.0.0.1 dan tutup port 3306/3309 di firewall.
  7. Hak akses file. File aplikasi dibuat read-only untuk user web server. config.php hanya bisa dibaca oleh user web server (misalnya chmod 640).
  8. Folder yang tidak perlu ada di server produksi (opsional, karena sudah diblokir): tests/, docs/, .impeccable/, PRODUCT.md, DESIGN.md.
  9. AllowOverride. Pastikan AllowOverride All (atau minimal AuthConfig FileInfo Indexes Limit Options) untuk folder aplikasi. Tanpa itu .htaccess diabaikan dan file internal bisa terbuka. Cek dengan membuka https://<server>/config.php; hasilnya harus 403/404.

Cek cepat setelah deploy

curl -sI https://<server>/ | grep -iE "server|x-powered|content-security|strict-transport"
curl -s -o /dev/null -w "%{http_code}\n" https://<server>/config.php        # 403
curl -s -o /dev/null -w "%{http_code}\n" -X TRACE https://<server>/         # 403/405
curl -s -o /dev/null -w "%{http_code}\n" https://<server>/.git/config       # 404

Hasil yang diharapkan:

  • Server: Apache, tanpa nomor versi.
  • Tidak ada X-Powered-By.
  • Ada Content-Security-Policy dan Strict-Transport-Security.

Upgrade ke versi 2.3.0 (sekali)

  1. Jalankan database/upgrade-2.3.0.sql di phpMyAdmin (tab SQL). Admin aktif paling lama menjadi super admin; periksa dengan SELECT id, username, role FROM admins;.
  2. Buat secrets.php dari database/secrets.example.php (chmod 600), isi kredensial database dan app_key. Letakkan di /home/<akun>/.serubi/ bila ada akses, atau di <folder aplikasi>/.serubi/ (folder ini sudah berisi .htaccess yang menolak akses HTTP). Cek: https://<server>/.serubi/secrets.php harus 403.
  3. Batasi user database aplikasi di cPanel menjadi SELECT, INSERT, UPDATE, DELETE saja.
  4. Unggah file aplikasi. Setiap admin akan diminta mengaktifkan authenticator saat login berikutnya.
  5. Uji: php tests/validate_engine.php dan node tests/validate.js harus 4.805/4.805; node tests/validate_public.js harus 3.100/3.100.
  6. Mode offline: sw.js (service worker) menyimpan halaman kalkulator. Model berumur lebih dari 30 hari ditolak; perangkat harus online untuk memperbarui.

Tindakan untuk akun admin yang sudah ada

Aturan password baru berlaku saat password dibuat atau diganti. Akun admin yang dibuat sebelum update ini sebaiknya mengganti password lewat menu Pengguna admin → Password Anda. Setelah update, semua admin yang sedang login akan diminta login ulang satu kali.