8.1 KiB
8.1 KiB
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.
- HTTPS. Pasang sertifikat TLS, lalu aktifkan pengalihan HTTP→HTTPS lewat environment
SERUBI_FORCE_HTTPS=1atau'force_https' => truediconfig.php. Izinkan hanya TLS 1.2/1.3:Kalau aplikasi berada di belakang reverse proxy atau load balancer, isi IP proxy tersebut diSSLProtocol -all +TLSv1.2 +TLSv1.3 SSLHonorCipherOrder offSERUBI_TRUSTED_PROXIES. - Sembunyikan versi dan matikan TRACE di
httpd.conf. Pengaturan ini tidak bisa lewat.htaccess:ServerTokens Prod ServerSignature Off TraceEnable Off - php.ini produksi:
expose_php = Off display_errors = Off log_errors = On session.cookie_secure = 1 session.use_strict_mode = 1 - 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.
- Jangan pakai user
rootuntuk MySQL. Jalankan sekali dengan akun root:Perintah ini membuat userphp database/migrate.php --create-db-userserubi_appyang hanya punya hak SELECT/INSERT/UPDATE/DELETE pada databasecalcserubi. Salin user dan password yang dicetak keconfig.phpatau ke environmentSERUBI_DB_USER/SERUBI_DB_PASS. Setelah itu ganti juga passwordrootMySQL, karena password yang sekarang pernah tertulis di config. - MySQL jangan terbuka ke internet. Set
bind-address = 127.0.0.1dan tutup port 3306/3309 di firewall. - Hak akses file. File aplikasi dibuat read-only untuk user web server.
config.phphanya bisa dibaca oleh user web server (misalnyachmod 640). - Folder yang tidak perlu ada di server produksi (opsional, karena sudah diblokir):
tests/,docs/,.impeccable/,PRODUCT.md,DESIGN.md. - AllowOverride. Pastikan
AllowOverride All(atau minimalAuthConfig FileInfo Indexes Limit Options) untuk folder aplikasi. Tanpa itu.htaccessdiabaikan dan file internal bisa terbuka. Cek dengan membukahttps://<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-PolicydanStrict-Transport-Security.
Upgrade ke versi 2.3.0 (sekali)
- Jalankan
database/upgrade-2.3.0.sqldi phpMyAdmin (tab SQL). Admin aktif paling lama menjadi super admin; periksa denganSELECT id, username, role FROM admins;. - Buat
secrets.phpdaridatabase/secrets.example.php(chmod 600), isi kredensial database danapp_key. Letakkan di/home/<akun>/.serubi/bila ada akses, atau di<folder aplikasi>/.serubi/(folder ini sudah berisi.htaccessyang menolak akses HTTP). Cek:https://<server>/.serubi/secrets.phpharus 403. - Batasi user database aplikasi di cPanel menjadi SELECT, INSERT, UPDATE, DELETE saja.
- Unggah file aplikasi. Setiap admin akan diminta mengaktifkan authenticator saat login berikutnya.
- Uji:
php tests/validate_engine.phpdannode tests/validate.jsharus 4.805/4.805;node tests/validate_public.jsharus 3.100/3.100. - 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.